siesa-agents 2.1.45 → 2.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,56 +1,410 @@
|
|
|
1
1
|
# Frontend Development Standards
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
3
|
+
## Resumen
|
|
4
|
+
|
|
5
|
+
Este documento define los estándares de desarrollo frontend para aplicaciones empresariales usando **Vite** como bundler, **TanStack Router** para enrutamiento type-safe, **Zustand** para estado global, y **Clean Architecture** con estructura modular preparada para microfrontends.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Tabla de Contenidos
|
|
10
|
+
|
|
11
|
+
1. [Principios de Arquitectura](#1-principios-de-arquitectura)
|
|
12
|
+
2. [Stack Tecnológico](#2-stack-tecnológico)
|
|
13
|
+
3. [Convenciones de Routing](#3-convenciones-de-routing)
|
|
14
|
+
4. [Organización de Archivos](#4-organización-de-archivos)
|
|
15
|
+
5. [Estándares de Componentes](#5-estándares-de-componentes)
|
|
16
|
+
6. [Patrones de Estado](#6-patrones-de-estado)
|
|
17
|
+
7. [Estándares de Testing](#7-estándares-de-testing)
|
|
18
|
+
8. [Accesibilidad](#8-accesibilidad)
|
|
19
|
+
9. [Rendimiento](#9-rendimiento)
|
|
20
|
+
10. [Seguridad](#10-seguridad)
|
|
21
|
+
11. [Manejo de Errores](#11-manejo-de-errores)
|
|
22
|
+
12. [Progressive Web App](#12-progressive-web-app)
|
|
23
|
+
13. [Estándares de Idioma](#13-estándares-de-idioma)
|
|
24
|
+
14. [Consideraciones Generales](#14-consideraciones-generales)
|
|
25
|
+
15. [Configuración Base](#15-configuración-base)
|
|
26
|
+
16. [Checklist de Implementación](#16-checklist-de-implementación)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. Principios de Arquitectura
|
|
31
|
+
|
|
32
|
+
### 1.1 Implementación de Clean Architecture
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
36
|
+
│ PRESENTATION LAYER │
|
|
37
|
+
│ React components, pages, UI logic, routes │
|
|
38
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
39
|
+
│
|
|
40
|
+
▼
|
|
41
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
42
|
+
│ APPLICATION LAYER │
|
|
43
|
+
│ Use cases, custom hooks, Zustand stores │
|
|
44
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
45
|
+
│
|
|
46
|
+
▼
|
|
47
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
48
|
+
│ INFRASTRUCTURE LAYER │
|
|
49
|
+
│ API clients, repositories impl, external adapters │
|
|
50
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
51
|
+
│
|
|
52
|
+
▼
|
|
53
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
54
|
+
│ DOMAIN LAYER │
|
|
55
|
+
│ Business entities, value objects, business rules │
|
|
56
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Capa | Responsabilidad | Contenido |
|
|
60
|
+
|------|-----------------|-----------|
|
|
61
|
+
| **Domain** | Reglas de negocio puras | Entities, value objects, interfaces de repositorios |
|
|
62
|
+
| **Application** | Orquestación de casos de uso | Use cases, hooks, stores Zustand |
|
|
63
|
+
| **Infrastructure** | Implementaciones externas | API clients, repositorios concretos, adapters |
|
|
64
|
+
| **Presentation** | Interfaz de usuario | Componentes React, páginas, estilos |
|
|
65
|
+
|
|
66
|
+
### 1.2 Reglas de Dependencia
|
|
67
|
+
|
|
68
|
+
- Las capas internas **NO deben conocer** las capas externas
|
|
69
|
+
- Las dependencias apuntan hacia adentro (de externo a interno)
|
|
70
|
+
- Usar inversión de dependencias para concerns externos
|
|
71
|
+
- Domain layer no importa nada de otras capas
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 2. Stack Tecnológico
|
|
76
|
+
|
|
77
|
+
### 2.1 Tecnologías Core
|
|
78
|
+
|
|
79
|
+
| Categoría | Tecnología | Versión | Notas |
|
|
80
|
+
|-----------|------------|---------|-------|
|
|
81
|
+
| **Bundler** | Vite | 5+ | Build tool y dev server |
|
|
82
|
+
| **Framework** | React | 18+ | Functional components y hooks |
|
|
83
|
+
| **Router** | TanStack Router | 1+ | File-based routing con type-safety |
|
|
84
|
+
| **Lenguaje** | TypeScript | 5+ | Strict mode, sin `any` |
|
|
85
|
+
| **Estilos** | TailwindCSS | 4+ | Utility-first CSS |
|
|
86
|
+
| **Componentes** | shadcn/ui + Radix UI | - | Base de componentes |
|
|
87
|
+
| **Estado** | Zustand | 4+ | Estado global por feature |
|
|
88
|
+
| **Data Fetching** | TanStack Query | 5+ | Cache y sincronización |
|
|
89
|
+
|
|
90
|
+
### 2.2 Reglas de Selección de Framework
|
|
91
|
+
|
|
92
|
+
| Escenario | Framework | Razón |
|
|
93
|
+
|-----------|-----------|-------|
|
|
94
|
+
| **Default** | Vite + TanStack Router | Mejor DX, type-safety, preparado para microfrontends |
|
|
95
|
+
| **Microfrontends** | Vite + Module Federation | Arquitectura distribuida |
|
|
96
|
+
|
|
97
|
+
### 2.3 Herramientas de Desarrollo
|
|
98
|
+
|
|
99
|
+
| Herramienta | Propósito |
|
|
100
|
+
|-------------|-----------|
|
|
101
|
+
| **Vite** | Build system y dev server |
|
|
102
|
+
| **Vitest** | Unit e integration testing |
|
|
103
|
+
| **React Testing Library** | Component testing |
|
|
104
|
+
| **MSW** | API mocking para tests |
|
|
105
|
+
| **ESLint + Prettier** | Code quality y formatting |
|
|
106
|
+
| **TypeScript** | Type checking |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 3. Convenciones de Routing
|
|
111
|
+
|
|
112
|
+
TanStack Router usa prefijos especiales en nombres de archivo para definir comportamientos.
|
|
113
|
+
|
|
114
|
+
### 3.1 Prefijo `_` (Underscore) - Pathless Layout Routes
|
|
115
|
+
|
|
116
|
+
**Propósito:** Agrupar rutas bajo un layout compartido **SIN agregar segmentos a la URL**.
|
|
117
|
+
|
|
118
|
+
#### ❌ El Problema (sin `_`)
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
routes/
|
|
122
|
+
├── __root.tsx
|
|
123
|
+
├── auth/
|
|
124
|
+
│ └── login.tsx → URL: /auth/login ❌
|
|
125
|
+
├── app/
|
|
126
|
+
│ ├── dashboard.tsx → URL: /app/dashboard ❌
|
|
127
|
+
│ └── orders.tsx → URL: /app/orders ❌
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Resultado:** Las URLs incluyen el nombre de la carpeta, lo cual es indeseable.
|
|
131
|
+
|
|
132
|
+
#### ✅ La Solución (con `_`)
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
routes/
|
|
136
|
+
├── __root.tsx
|
|
137
|
+
├── _auth.tsx → NO agrega nada a la URL (solo layout)
|
|
138
|
+
├── _auth/
|
|
139
|
+
│ └── login.tsx → URL: /login ✅
|
|
140
|
+
│
|
|
141
|
+
├── _app.tsx → NO agrega nada a la URL (solo layout)
|
|
142
|
+
├── _app/
|
|
143
|
+
│ ├── dashboard.tsx → URL: /dashboard ✅
|
|
144
|
+
│ └── orders.tsx → URL: /orders ✅
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Ejemplo de Implementación
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// routes/_app.tsx - Layout protegido
|
|
151
|
+
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
|
|
152
|
+
import { Sidebar, TopNav } from '@/shared/components/layout';
|
|
153
|
+
import { useAuthStore } from '@/modules/users/authentication/login/application/store';
|
|
154
|
+
|
|
155
|
+
export const Route = createFileRoute('/_app')({
|
|
156
|
+
beforeLoad: () => {
|
|
157
|
+
const { isAuthenticated } = useAuthStore.getState();
|
|
158
|
+
if (!isAuthenticated) {
|
|
159
|
+
throw redirect({ to: '/login' });
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
component: AppLayout,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
function AppLayout() {
|
|
166
|
+
return (
|
|
167
|
+
<div className="flex h-screen">
|
|
168
|
+
<Sidebar />
|
|
169
|
+
<div className="flex-1 flex flex-col">
|
|
170
|
+
<TopNav />
|
|
171
|
+
<main className="flex-1 overflow-auto p-6">
|
|
172
|
+
<Outlet />
|
|
173
|
+
</main>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 3.2 Prefijo `.` (Punto) - Flat Routing
|
|
181
|
+
|
|
182
|
+
**Propósito:** Definir rutas anidadas **sin crear carpetas**.
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
routes/
|
|
186
|
+
├── orders.tsx → /orders (layout)
|
|
187
|
+
├── orders.index.tsx → /orders
|
|
188
|
+
├── orders.$orderId.tsx → /orders/:orderId
|
|
189
|
+
├── orders.$orderId.edit.tsx → /orders/:orderId/edit
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### ¿Cuándo usar `.` vs Carpetas?
|
|
193
|
+
|
|
194
|
+
| Escenario | Recomendación |
|
|
195
|
+
|-----------|---------------|
|
|
196
|
+
| Pocas rutas anidadas (2-3) | Flat con `.` |
|
|
197
|
+
| Muchas rutas anidadas (4+) | Carpetas |
|
|
198
|
+
| Rutas con componentes colocados | Carpetas con `-components/` |
|
|
199
|
+
|
|
200
|
+
### 3.3 Prefijo `-` (Guión) - Ignorar Archivos
|
|
201
|
+
|
|
202
|
+
**Propósito:** Excluir archivos/carpetas de la generación de rutas para colocación de código.
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
routes/
|
|
206
|
+
├── orders/
|
|
207
|
+
│ ├── $orderId.tsx → /orders/:orderId ✅
|
|
208
|
+
│ ├── -components/ → ❌ Ignorado por el router
|
|
209
|
+
│ │ └── OrderHeader.tsx
|
|
210
|
+
│ └── -hooks/ → ❌ Ignorado por el router
|
|
211
|
+
│ └── useOrderCalculations.ts
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 3.4 Prefijo `$` (Dólar) - Parámetros Dinámicos
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// routes/_app/orders/$orderId.tsx
|
|
218
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
219
|
+
|
|
220
|
+
export const Route = createFileRoute('/_app/orders/$orderId')({
|
|
221
|
+
component: OrderDetail,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
function OrderDetail() {
|
|
225
|
+
const { orderId } = Route.useParams(); // Tipado automático
|
|
226
|
+
return <div>Order: {orderId}</div>;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 3.5 Resumen de Prefijos
|
|
231
|
+
|
|
232
|
+
| Prefijo | Nombre | Efecto en URL | Uso Principal |
|
|
233
|
+
|---------|--------|---------------|---------------|
|
|
234
|
+
| `_` | Pathless | **No aparece** | Layouts sin path |
|
|
235
|
+
| `.` | Flat | Crea anidamiento | Evitar carpetas |
|
|
236
|
+
| `-` | Ignore | **No genera ruta** | Colocación de código |
|
|
237
|
+
| `$` | Dynamic | Captura valor | Parámetros de URL |
|
|
238
|
+
| `__` | Root | Raíz del árbol | Solo `__root.tsx` |
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 4. Organización de Archivos
|
|
243
|
+
|
|
244
|
+
### 4.1 Estructura Enterprise (Module/Domain/Feature)
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
src/
|
|
248
|
+
├── main.tsx # React entry point
|
|
249
|
+
├── router.tsx # TanStack Router config
|
|
250
|
+
├── routeTree.gen.ts # Auto-generado (NO EDITAR)
|
|
251
|
+
├── globals.css # Estilos globales + Tailwind
|
|
252
|
+
│
|
|
253
|
+
├── routes/ # 🛣️ SOLO definición de rutas
|
|
254
|
+
│ ├── __root.tsx # Layout raíz (providers)
|
|
255
|
+
│ ├── index.tsx # Redirect inicial
|
|
256
|
+
│ ├── _auth.tsx # Layout público
|
|
257
|
+
│ ├── _auth/
|
|
258
|
+
│ │ └── login.tsx
|
|
259
|
+
│ ├── _app.tsx # Layout protegido
|
|
260
|
+
│ └── _app/
|
|
261
|
+
│ ├── dashboard.tsx
|
|
262
|
+
│ ├── sales/
|
|
263
|
+
│ │ ├── quotes.tsx
|
|
264
|
+
│ │ └── invoices.tsx
|
|
265
|
+
│ └── inventory/
|
|
266
|
+
│ └── products.tsx
|
|
267
|
+
│
|
|
268
|
+
├── modules/ # 🏢 Lógica de negocio por módulo
|
|
269
|
+
│ ├── sales/ # MODULE
|
|
270
|
+
│ │ ├── quotes/ # DOMAIN
|
|
271
|
+
│ │ │ ├── cart/ # FEATURE
|
|
272
|
+
│ │ │ │ ├── domain/
|
|
273
|
+
│ │ │ │ │ ├── entities/
|
|
274
|
+
│ │ │ │ │ │ └── CartItem.ts
|
|
275
|
+
│ │ │ │ │ ├── repositories/ # Interfaces
|
|
276
|
+
│ │ │ │ │ │ └── ICartRepository.ts
|
|
277
|
+
│ │ │ │ │ ├── services/ # Domain services
|
|
278
|
+
│ │ │ │ │ │ └── CartCalculator.ts
|
|
279
|
+
│ │ │ │ │ └── types/
|
|
280
|
+
│ │ │ │ │ └── cart.types.ts
|
|
281
|
+
│ │ │ │ ├── application/
|
|
282
|
+
│ │ │ │ │ ├── use-cases/
|
|
283
|
+
│ │ │ │ │ │ ├── AddToCart.ts
|
|
284
|
+
│ │ │ │ │ │ └── RemoveFromCart.ts
|
|
285
|
+
│ │ │ │ │ ├── hooks/
|
|
286
|
+
│ │ │ │ │ │ └── useCart.ts
|
|
287
|
+
│ │ │ │ │ └── store/
|
|
288
|
+
│ │ │ │ │ └── cart.store.ts
|
|
289
|
+
│ │ │ │ ├── infrastructure/
|
|
290
|
+
│ │ │ │ │ ├── repositories/ # Implementations
|
|
291
|
+
│ │ │ │ │ │ └── CartRepository.ts
|
|
292
|
+
│ │ │ │ │ ├── api/
|
|
293
|
+
│ │ │ │ │ │ └── cart.api.ts
|
|
294
|
+
│ │ │ │ │ └── adapters/
|
|
295
|
+
│ │ │ │ └── presentation/
|
|
296
|
+
│ │ │ │ ├── components/
|
|
297
|
+
│ │ │ │ │ ├── CartList.tsx
|
|
298
|
+
│ │ │ │ │ └── CartItem.tsx
|
|
299
|
+
│ │ │ │ └── pages/
|
|
300
|
+
│ │ │ │ └── CartPage.tsx
|
|
301
|
+
│ │ │ └── products/ # FEATURE
|
|
302
|
+
│ │ │ ├── domain/
|
|
303
|
+
│ │ │ ├── application/
|
|
304
|
+
│ │ │ ├── infrastructure/
|
|
305
|
+
│ │ │ └── presentation/
|
|
306
|
+
│ │ └── billing/ # DOMAIN
|
|
307
|
+
│ │ ├── invoices/ # FEATURE
|
|
308
|
+
│ │ └── reports/ # FEATURE
|
|
309
|
+
│ │
|
|
310
|
+
│ ├── inventory/ # MODULE
|
|
311
|
+
│ │ ├── products/ # DOMAIN
|
|
312
|
+
│ │ │ ├── catalog/ # FEATURE
|
|
313
|
+
│ │ │ └── stock/ # FEATURE
|
|
314
|
+
│ │ └── warehouses/ # DOMAIN
|
|
315
|
+
│ │
|
|
316
|
+
│ └── users/ # MODULE
|
|
317
|
+
│ └── authentication/ # DOMAIN
|
|
318
|
+
│ ├── login/ # FEATURE
|
|
319
|
+
│ └── registration/ # FEATURE
|
|
320
|
+
│
|
|
321
|
+
├── shared/ # 🔄 Código reutilizable
|
|
322
|
+
│ ├── components/
|
|
323
|
+
│ │ └── ui/ # shadcn/ui + siesa-ui-kit
|
|
324
|
+
│ ├── hooks/
|
|
325
|
+
│ │ ├── useDebounce.ts
|
|
326
|
+
│ │ └── useLocalStorage.ts
|
|
327
|
+
│ ├── lib/
|
|
328
|
+
│ │ ├── utils.ts # cn(), formatters
|
|
329
|
+
│ │ ├── api-client.ts # Axios/fetch config
|
|
330
|
+
│ │ └── env.ts # Variables de entorno tipadas
|
|
331
|
+
│ ├── types/
|
|
332
|
+
│ │ └── common.types.ts
|
|
333
|
+
│ └── constants/
|
|
334
|
+
│
|
|
335
|
+
├── app/ # 🎯 Configuración global
|
|
336
|
+
│ ├── store/ # Global store (si necesario)
|
|
337
|
+
│ ├── providers/ # Context providers
|
|
338
|
+
│ │ ├── ThemeProvider.tsx
|
|
339
|
+
│ │ └── QueryProvider.tsx
|
|
340
|
+
│ └── config/
|
|
341
|
+
│
|
|
342
|
+
├── infrastructure/ # 🔌 Servicios externos globales
|
|
343
|
+
│ ├── api/ # API configuration
|
|
344
|
+
│ ├── storage/ # IndexedDB, localStorage
|
|
345
|
+
│ └── pwa/ # PWA configuration
|
|
346
|
+
│
|
|
347
|
+
├── assets/ # 📁 Recursos estáticos
|
|
348
|
+
│ ├── fonts/
|
|
349
|
+
│ │ ├── SiesaBT-Light.otf
|
|
350
|
+
│ │ ├── SiesaBT-Regular.otf
|
|
351
|
+
│ │ └── SiesaBT-Bold.otf
|
|
352
|
+
│ └── images/
|
|
353
|
+
│ └── logos/
|
|
354
|
+
│
|
|
355
|
+
└── styles/
|
|
356
|
+
└── globals.css
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### 4.2 Jerarquía de Carpetas
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
MODULE (módulo de negocio)
|
|
363
|
+
└── DOMAIN (área funcional)
|
|
364
|
+
└── FEATURE (funcionalidad específica)
|
|
365
|
+
├── domain/ # Reglas de negocio
|
|
366
|
+
├── application/ # Casos de uso y estado
|
|
367
|
+
├── infrastructure/ # Implementaciones externas
|
|
368
|
+
└── presentation/ # UI
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### 4.3 Organización de Imports
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
// 1. Librerías externas
|
|
375
|
+
import React from 'react';
|
|
376
|
+
import { create } from 'zustand';
|
|
377
|
+
import { useQuery } from '@tanstack/react-query';
|
|
378
|
+
|
|
379
|
+
// 2. Módulos internos (por capa, de interno a externo)
|
|
380
|
+
import { CartItem } from '../domain/entities/CartItem';
|
|
381
|
+
import { addToCartUseCase } from '../application/use-cases/AddToCart';
|
|
382
|
+
import { cartRepository } from '../infrastructure/repositories/CartRepository';
|
|
383
|
+
|
|
384
|
+
// 3. Shared
|
|
385
|
+
import { cn } from '@/shared/lib/utils';
|
|
386
|
+
import { Button } from '@/shared/components/ui/button';
|
|
387
|
+
|
|
388
|
+
// 4. Types
|
|
389
|
+
import type { Cart } from '../domain/types/cart.types';
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 5. Estándares de Componentes
|
|
395
|
+
|
|
396
|
+
### 5.1 Estrategia de Componentes
|
|
397
|
+
|
|
398
|
+
| Prioridad | Acción |
|
|
399
|
+
|-----------|--------|
|
|
400
|
+
| **1. siesa-ui-kit** | Siempre verificar primero si existe el componente |
|
|
401
|
+
| **2. Si no existe** | Preguntar al usuario: [1] Usar shadcn directamente, [2] Crear para siesa-ui-kit (requiere MR) |
|
|
402
|
+
| **3. Shadcn fallback** | Solo usar registro MCP Shadcn si el usuario elige opción [1] |
|
|
403
|
+
|
|
404
|
+
> **Beneficio:** 90% menos bugs usando componentes existentes vs creación manual.
|
|
405
|
+
|
|
406
|
+
### 5.2 Estructura de Componentes
|
|
407
|
+
|
|
54
408
|
```typescript
|
|
55
409
|
interface ComponentProps {
|
|
56
410
|
// Required props
|
|
@@ -67,9 +421,11 @@ export const Component = memo<ComponentProps>(({
|
|
|
67
421
|
variant = 'default',
|
|
68
422
|
'data-testid': testId = 'component'
|
|
69
423
|
}) => {
|
|
70
|
-
// Component implementation
|
|
71
424
|
return (
|
|
72
|
-
<div
|
|
425
|
+
<div
|
|
426
|
+
className={cn(baseStyles, variantStyles[variant], className)}
|
|
427
|
+
data-testid={testId}
|
|
428
|
+
>
|
|
73
429
|
{children}
|
|
74
430
|
</div>
|
|
75
431
|
);
|
|
@@ -78,299 +434,742 @@ export const Component = memo<ComponentProps>(({
|
|
|
78
434
|
Component.displayName = 'Component';
|
|
79
435
|
```
|
|
80
436
|
|
|
81
|
-
###
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
437
|
+
### 5.3 Convenciones de Nombrado
|
|
438
|
+
|
|
439
|
+
| Elemento | Convención | Ejemplo |
|
|
440
|
+
|----------|------------|---------|
|
|
441
|
+
| Componentes | PascalCase | `OrderCard.tsx` |
|
|
442
|
+
| Archivos | kebab-case | `order-card.tsx` |
|
|
443
|
+
| data-testid | kebab-case | `data-testid="order-card"` |
|
|
444
|
+
| Props/funciones | camelCase | `onSubmit`, `isLoading` |
|
|
445
|
+
| Constantes | SCREAMING_SNAKE | `MAX_ITEMS` |
|
|
446
|
+
|
|
447
|
+
### 5.4 Guidelines de Props
|
|
448
|
+
|
|
449
|
+
- Siempre definir interfaces TypeScript para props
|
|
450
|
+
- Usar props opcionales con defaults sensatos
|
|
451
|
+
- Incluir `className` para overrides de estilos
|
|
452
|
+
- Agregar `data-testid` para testing
|
|
453
|
+
- Documentar props complejas con JSDoc
|
|
454
|
+
|
|
455
|
+
---
|
|
86
456
|
|
|
87
|
-
|
|
88
|
-
- Always define TypeScript interfaces for props
|
|
89
|
-
- Use optional props with sensible defaults
|
|
90
|
-
- Include className for style overrides
|
|
91
|
-
- Add data-testid for testing
|
|
92
|
-
- Document complex props with JSDoc
|
|
457
|
+
## 6. Patrones de Estado
|
|
93
458
|
|
|
94
|
-
|
|
459
|
+
### 6.1 Estructura de Zustand Store
|
|
95
460
|
|
|
96
|
-
### Zustand Store Structure
|
|
97
461
|
```typescript
|
|
98
|
-
|
|
462
|
+
// modules/sales/quotes/cart/application/store/cart.store.ts
|
|
463
|
+
import { create } from 'zustand';
|
|
464
|
+
import { devtools } from 'zustand/middleware';
|
|
465
|
+
import type { CartItem } from '../../domain/entities/CartItem';
|
|
466
|
+
import { addToCartUseCase } from '../use-cases/AddToCart';
|
|
467
|
+
import { removeFromCartUseCase } from '../use-cases/RemoveFromCart';
|
|
468
|
+
|
|
469
|
+
interface CartState {
|
|
99
470
|
// Domain entities
|
|
100
|
-
|
|
101
|
-
selectedEntity: Entity | null;
|
|
471
|
+
items: CartItem[];
|
|
102
472
|
|
|
103
473
|
// UI state
|
|
104
474
|
loading: boolean;
|
|
105
475
|
error: string | null;
|
|
106
476
|
|
|
107
|
-
// Actions (use
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
477
|
+
// Actions (delegan a use cases)
|
|
478
|
+
addItem: (productId: string, quantity: number) => Promise<void>;
|
|
479
|
+
removeItem: (itemId: string) => Promise<void>;
|
|
480
|
+
clearCart: () => void;
|
|
111
481
|
clearError: () => void;
|
|
112
482
|
}
|
|
113
483
|
|
|
114
|
-
export const
|
|
484
|
+
export const useCartStore = create<CartState>()(
|
|
115
485
|
devtools(
|
|
116
486
|
(set, get) => ({
|
|
117
487
|
// Initial state
|
|
118
|
-
|
|
119
|
-
selectedEntity: null,
|
|
488
|
+
items: [],
|
|
120
489
|
loading: false,
|
|
121
490
|
error: null,
|
|
122
491
|
|
|
123
|
-
// Actions
|
|
124
|
-
|
|
492
|
+
// Actions
|
|
493
|
+
addItem: async (productId, quantity) => {
|
|
494
|
+
set({ loading: true, error: null });
|
|
495
|
+
try {
|
|
496
|
+
const newItem = await addToCartUseCase.execute({ productId, quantity });
|
|
497
|
+
set((state) => ({
|
|
498
|
+
items: [...state.items, newItem],
|
|
499
|
+
loading: false
|
|
500
|
+
}));
|
|
501
|
+
} catch (error) {
|
|
502
|
+
set({ error: (error as Error).message, loading: false });
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
removeItem: async (itemId) => {
|
|
125
507
|
set({ loading: true, error: null });
|
|
126
508
|
try {
|
|
127
|
-
|
|
128
|
-
set(
|
|
509
|
+
await removeFromCartUseCase.execute(itemId);
|
|
510
|
+
set((state) => ({
|
|
511
|
+
items: state.items.filter(item => item.id !== itemId),
|
|
512
|
+
loading: false
|
|
513
|
+
}));
|
|
129
514
|
} catch (error) {
|
|
130
|
-
set({ error: error.message, loading: false });
|
|
515
|
+
set({ error: (error as Error).message, loading: false });
|
|
131
516
|
}
|
|
132
517
|
},
|
|
133
518
|
|
|
134
|
-
|
|
519
|
+
clearCart: () => set({ items: [] }),
|
|
520
|
+
clearError: () => set({ error: null }),
|
|
135
521
|
}),
|
|
136
|
-
{ name: '
|
|
522
|
+
{ name: 'cartStore' }
|
|
137
523
|
)
|
|
138
524
|
);
|
|
139
525
|
```
|
|
140
526
|
|
|
141
|
-
|
|
527
|
+
### 6.2 Cuándo Usar Cada Tipo de Estado
|
|
528
|
+
|
|
529
|
+
| Tipo | Cuándo Usar | Herramienta |
|
|
530
|
+
|------|-------------|-------------|
|
|
531
|
+
| **Server State** | Datos del API, cache | TanStack Query |
|
|
532
|
+
| **Global Client State** | Auth, theme, cart | Zustand |
|
|
533
|
+
| **Local Component State** | Forms, UI toggles | useState/useReducer |
|
|
534
|
+
| **URL State** | Filtros, paginación | TanStack Router search params |
|
|
535
|
+
|
|
536
|
+
### 6.3 Integración con TanStack Query
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// modules/sales/quotes/products/infrastructure/api/products.api.ts
|
|
540
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
541
|
+
import { productRepository } from '../repositories/ProductRepository';
|
|
542
|
+
|
|
543
|
+
export const productKeys = {
|
|
544
|
+
all: ['products'] as const,
|
|
545
|
+
lists: () => [...productKeys.all, 'list'] as const,
|
|
546
|
+
list: (filters: ProductFilters) => [...productKeys.lists(), filters] as const,
|
|
547
|
+
detail: (id: string) => [...productKeys.all, 'detail', id] as const,
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
export function useProducts(filters: ProductFilters) {
|
|
551
|
+
return useQuery({
|
|
552
|
+
queryKey: productKeys.list(filters),
|
|
553
|
+
queryFn: () => productRepository.getAll(filters),
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export function useProduct(id: string) {
|
|
558
|
+
return useQuery({
|
|
559
|
+
queryKey: productKeys.detail(id),
|
|
560
|
+
queryFn: () => productRepository.getById(id),
|
|
561
|
+
enabled: !!id,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## 7. Estándares de Testing
|
|
569
|
+
|
|
570
|
+
### 7.1 Estrategia de Testing
|
|
571
|
+
|
|
572
|
+
| Tipo | Cobertura | Herramienta | Qué Testear |
|
|
573
|
+
|------|-----------|-------------|-------------|
|
|
574
|
+
| **Unit** | Alta | Vitest | Entities, use cases, utilities |
|
|
575
|
+
| **Integration** | Media | Vitest + MSW | Feature workflows, API integration |
|
|
576
|
+
| **Component** | Media | React Testing Library | User interactions, accessibility |
|
|
577
|
+
| **E2E** | Baja | Playwright | Critical user journeys |
|
|
578
|
+
|
|
579
|
+
### 7.2 Estructura de Tests
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
// modules/sales/quotes/cart/application/use-cases/__tests__/AddToCart.test.ts
|
|
583
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
584
|
+
import { addToCartUseCase } from '../AddToCart';
|
|
585
|
+
import { mockCartRepository } from '../../__mocks__/cartRepository.mock';
|
|
586
|
+
|
|
587
|
+
describe('AddToCart UseCase', () => {
|
|
588
|
+
beforeEach(() => {
|
|
589
|
+
vi.clearAllMocks();
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('should add item to cart successfully', async () => {
|
|
593
|
+
const input = { productId: 'prod-1', quantity: 2 };
|
|
594
|
+
|
|
595
|
+
const result = await addToCartUseCase.execute(input);
|
|
596
|
+
|
|
597
|
+
expect(result).toMatchObject({
|
|
598
|
+
productId: 'prod-1',
|
|
599
|
+
quantity: 2,
|
|
600
|
+
});
|
|
601
|
+
expect(mockCartRepository.save).toHaveBeenCalledWith(expect.objectContaining(input));
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should throw error when quantity is invalid', async () => {
|
|
605
|
+
const input = { productId: 'prod-1', quantity: -1 };
|
|
606
|
+
|
|
607
|
+
await expect(addToCartUseCase.execute(input)).rejects.toThrow(
|
|
608
|
+
'La cantidad debe ser mayor a 0'
|
|
609
|
+
);
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
```
|
|
142
613
|
|
|
143
|
-
### Testing
|
|
144
|
-
- **Unit Tests**: Domain entities, use cases, utilities
|
|
145
|
-
- **Integration Tests**: Feature workflows, API integration
|
|
146
|
-
- **Component Tests**: User interactions, accessibility
|
|
147
|
-
- **E2E Tests**: Critical user journeys (minimal)
|
|
614
|
+
### 7.3 Component Testing
|
|
148
615
|
|
|
149
|
-
### Test Structure
|
|
150
616
|
```typescript
|
|
151
|
-
|
|
617
|
+
// modules/sales/quotes/cart/presentation/components/__tests__/CartItem.test.tsx
|
|
618
|
+
import { describe, it, expect } from 'vitest';
|
|
619
|
+
import { render, screen } from '@testing-library/react';
|
|
620
|
+
import userEvent from '@testing-library/user-event';
|
|
621
|
+
import { axe } from 'vitest-axe';
|
|
622
|
+
import { CartItem } from '../CartItem';
|
|
623
|
+
|
|
624
|
+
describe('CartItem', () => {
|
|
625
|
+
const defaultProps = {
|
|
626
|
+
item: { id: '1', name: 'Producto Test', quantity: 2, price: 100 },
|
|
627
|
+
onRemove: vi.fn(),
|
|
628
|
+
};
|
|
629
|
+
|
|
152
630
|
it('should render correctly with default props', () => {
|
|
153
|
-
render(<
|
|
154
|
-
|
|
631
|
+
render(<CartItem {...defaultProps} />);
|
|
632
|
+
|
|
633
|
+
expect(screen.getByTestId('cart-item')).toBeInTheDocument();
|
|
634
|
+
expect(screen.getByText('Producto Test')).toBeInTheDocument();
|
|
155
635
|
});
|
|
156
|
-
|
|
157
|
-
it('should handle
|
|
158
|
-
const
|
|
159
|
-
render(<
|
|
636
|
+
|
|
637
|
+
it('should handle remove action', async () => {
|
|
638
|
+
const user = userEvent.setup();
|
|
639
|
+
render(<CartItem {...defaultProps} />);
|
|
160
640
|
|
|
161
|
-
await user.click(screen.getByRole('button'));
|
|
162
|
-
|
|
641
|
+
await user.click(screen.getByRole('button', { name: /eliminar/i }));
|
|
642
|
+
|
|
643
|
+
expect(defaultProps.onRemove).toHaveBeenCalledWith('1');
|
|
163
644
|
});
|
|
164
|
-
|
|
645
|
+
|
|
165
646
|
it('should be accessible', async () => {
|
|
166
|
-
const { container } = render(<
|
|
647
|
+
const { container } = render(<CartItem {...defaultProps} />);
|
|
167
648
|
const results = await axe(container);
|
|
649
|
+
|
|
168
650
|
expect(results).toHaveNoViolations();
|
|
169
651
|
});
|
|
170
652
|
});
|
|
171
653
|
```
|
|
172
654
|
|
|
173
|
-
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## 8. Accesibilidad
|
|
658
|
+
|
|
659
|
+
### 8.1 Cumplimiento WCAG 2.1 AA
|
|
660
|
+
|
|
661
|
+
| Requisito | Implementación |
|
|
662
|
+
|-----------|----------------|
|
|
663
|
+
| Elementos semánticos | Usar HTML semántico (`<nav>`, `<main>`, `<article>`) |
|
|
664
|
+
| Jerarquía de headings | Un solo `<h1>`, headings en orden descendente |
|
|
665
|
+
| Navegación por teclado | Todos los elementos interactivos accesibles con Tab |
|
|
666
|
+
| Screen readers | Labels descriptivos, roles ARIA cuando necesario |
|
|
667
|
+
| Contraste de color | Mínimo 4.5:1 para texto normal |
|
|
174
668
|
|
|
175
|
-
###
|
|
176
|
-
- Semantic HTML elements
|
|
177
|
-
- Proper heading hierarchy
|
|
178
|
-
- Keyboard navigation support
|
|
179
|
-
- Screen reader compatibility
|
|
180
|
-
- Color contrast minimum 4.5:1
|
|
669
|
+
### 8.2 Implementación ARIA
|
|
181
670
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
-
|
|
187
|
-
|
|
671
|
+
```typescript
|
|
672
|
+
// ✅ Correcto - HTML semántico primero
|
|
673
|
+
<button onClick={handleSubmit}>Guardar</button>
|
|
674
|
+
|
|
675
|
+
// ✅ Correcto - ARIA cuando es necesario
|
|
676
|
+
<div
|
|
677
|
+
role="tabpanel"
|
|
678
|
+
aria-labelledby="tab-1"
|
|
679
|
+
aria-expanded={isOpen}
|
|
680
|
+
>
|
|
681
|
+
{content}
|
|
682
|
+
</div>
|
|
188
683
|
|
|
189
|
-
|
|
684
|
+
// ❌ Incorrecto - ARIA innecesario
|
|
685
|
+
<button role="button" aria-label="button">Guardar</button>
|
|
686
|
+
```
|
|
190
687
|
|
|
191
|
-
|
|
192
|
-
- Code splitting by route and feature
|
|
193
|
-
- Lazy loading for non-critical components
|
|
194
|
-
- Tree shaking for unused code
|
|
195
|
-
- Bundle size budget: 500KB total
|
|
688
|
+
---
|
|
196
689
|
|
|
197
|
-
|
|
198
|
-
- React.memo for expensive components
|
|
199
|
-
- useMemo for expensive calculations
|
|
200
|
-
- useCallback for event handlers passed to children
|
|
201
|
-
- Virtual scrolling for large lists
|
|
690
|
+
## 9. Rendimiento
|
|
202
691
|
|
|
203
|
-
###
|
|
204
|
-
- Image optimization and lazy loading
|
|
205
|
-
- Progressive loading strategies
|
|
206
|
-
- Skeleton screens for loading states
|
|
207
|
-
- Prefetch critical resources
|
|
692
|
+
### 9.1 Optimización de Bundle
|
|
208
693
|
|
|
209
|
-
|
|
694
|
+
| Estrategia | Implementación |
|
|
695
|
+
|------------|----------------|
|
|
696
|
+
| Code splitting | Por ruta (automático con TanStack Router) |
|
|
697
|
+
| Lazy loading | `React.lazy()` para componentes no críticos |
|
|
698
|
+
| Tree shaking | Imports específicos, no barrel exports en shared |
|
|
699
|
+
| Bundle budget | Máximo 500KB total |
|
|
210
700
|
|
|
211
|
-
###
|
|
212
|
-
- No API keys or secrets in frontend code
|
|
213
|
-
- Input validation and sanitization
|
|
214
|
-
- XSS prevention measures
|
|
215
|
-
- Secure handling of user data
|
|
701
|
+
### 9.2 Rendimiento en Runtime
|
|
216
702
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
703
|
+
```typescript
|
|
704
|
+
// ✅ React.memo para componentes costosos
|
|
705
|
+
export const ExpensiveList = memo<ListProps>(({ items }) => {
|
|
706
|
+
return items.map(item => <ExpensiveItem key={item.id} item={item} />);
|
|
707
|
+
});
|
|
222
708
|
|
|
223
|
-
|
|
709
|
+
// ✅ useMemo para cálculos costosos
|
|
710
|
+
const sortedItems = useMemo(() =>
|
|
711
|
+
items.sort((a, b) => a.name.localeCompare(b.name)),
|
|
712
|
+
[items]
|
|
713
|
+
);
|
|
224
714
|
|
|
225
|
-
|
|
715
|
+
// ✅ useCallback para handlers pasados a children
|
|
716
|
+
const handleClick = useCallback((id: string) => {
|
|
717
|
+
onSelect(id);
|
|
718
|
+
}, [onSelect]);
|
|
226
719
|
```
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
│ ├── login/ # FEATURE
|
|
265
|
-
│ └── registration/ # FEATURE
|
|
266
|
-
├── shared/
|
|
267
|
-
│ ├── components/ # Shadcn/ui components
|
|
268
|
-
│ │ └── ui/ # Base UI Kit components from proyecto/components/ui/
|
|
269
|
-
│ ├── hooks/
|
|
270
|
-
│ ├── utils/
|
|
271
|
-
│ ├── types/
|
|
272
|
-
│ └── constants/
|
|
273
|
-
├── app/
|
|
274
|
-
│ ├── store/ # Global store
|
|
275
|
-
│ ├── providers/ # Context providers
|
|
276
|
-
│ ├── router/ # Routing config
|
|
277
|
-
│ └── app.tsx
|
|
278
|
-
├── infrastructure/
|
|
279
|
-
│ ├── api/ # API configuration
|
|
280
|
-
│ ├── storage/ # IndexedDB, localStorage
|
|
281
|
-
│ └── pwa/ # PWA configuration
|
|
282
|
-
├── assets/
|
|
283
|
-
│ ├── fonts/ # Fonts from proyecto/public/fonts/
|
|
284
|
-
│ │ ├── SiesaBT-Light.otf
|
|
285
|
-
│ │ ├── SiesaBT-Regular.otf
|
|
286
|
-
│ │ └── SiesaBT-Bold.otf
|
|
287
|
-
│ ├── images/
|
|
288
|
-
│ │ └── logos/ # Logos from proyecto/public/images/logos/
|
|
289
|
-
│ │ ├── Siesa_Logosimbolo_Azul.svg
|
|
290
|
-
│ │ ├── Siesa_Logosimbolo_Blanco.svg
|
|
291
|
-
│ │ ├── Siesa_Simbolo_Azul.svg
|
|
292
|
-
│ │ └── Siesa_Simbolo_Blanco.svg
|
|
293
|
-
│ └── favicon.ico # Favicon from proyecto/public/favicon.ico
|
|
294
|
-
└── styles/
|
|
295
|
-
└── globals.css # Consolidated styles + Tailwind + WCAG from proyecto/styles/globals.css
|
|
720
|
+
|
|
721
|
+
### 9.3 Loading Performance
|
|
722
|
+
|
|
723
|
+
- Optimización de imágenes y lazy loading
|
|
724
|
+
- Skeleton screens para estados de carga
|
|
725
|
+
- Prefetch de recursos críticos
|
|
726
|
+
- Virtual scrolling para listas grandes
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## 10. Seguridad
|
|
731
|
+
|
|
732
|
+
### 10.1 Seguridad Client-Side
|
|
733
|
+
|
|
734
|
+
| Riesgo | Mitigación |
|
|
735
|
+
|--------|------------|
|
|
736
|
+
| API keys expuestas | Nunca en código frontend, usar variables de entorno server-side |
|
|
737
|
+
| XSS | Sanitización de inputs, no usar `dangerouslySetInnerHTML` |
|
|
738
|
+
| Datos sensibles | No almacenar en localStorage sin encriptar |
|
|
739
|
+
|
|
740
|
+
### 10.2 Autenticación
|
|
741
|
+
|
|
742
|
+
```typescript
|
|
743
|
+
// Manejo seguro de tokens
|
|
744
|
+
const useAuthStore = create<AuthState>((set) => ({
|
|
745
|
+
token: null,
|
|
746
|
+
|
|
747
|
+
setToken: (token: string) => {
|
|
748
|
+
// Almacenar en memoria, no localStorage
|
|
749
|
+
set({ token });
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
logout: () => {
|
|
753
|
+
set({ token: null });
|
|
754
|
+
// Limpiar cualquier dato sensible
|
|
755
|
+
},
|
|
756
|
+
}));
|
|
296
757
|
```
|
|
297
758
|
|
|
298
|
-
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## 11. Manejo de Errores
|
|
762
|
+
|
|
763
|
+
### 11.1 Error Boundaries por Feature
|
|
764
|
+
|
|
299
765
|
```typescript
|
|
300
|
-
//
|
|
301
|
-
import
|
|
302
|
-
|
|
766
|
+
// shared/components/feedback/ErrorBoundary.tsx
|
|
767
|
+
import { Component, ErrorInfo, ReactNode } from 'react';
|
|
768
|
+
|
|
769
|
+
interface Props {
|
|
770
|
+
children: ReactNode;
|
|
771
|
+
fallback?: ReactNode;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
interface State {
|
|
775
|
+
hasError: boolean;
|
|
776
|
+
error: Error | null;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
780
|
+
state: State = { hasError: false, error: null };
|
|
303
781
|
|
|
304
|
-
|
|
305
|
-
|
|
782
|
+
static getDerivedStateFromError(error: Error): State {
|
|
783
|
+
return { hasError: true, error };
|
|
784
|
+
}
|
|
306
785
|
|
|
307
|
-
|
|
308
|
-
|
|
786
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
787
|
+
console.error('Error capturado:', error, errorInfo);
|
|
788
|
+
// Enviar a servicio de logging
|
|
789
|
+
}
|
|
309
790
|
|
|
310
|
-
|
|
311
|
-
|
|
791
|
+
render() {
|
|
792
|
+
if (this.state.hasError) {
|
|
793
|
+
return this.props.fallback || (
|
|
794
|
+
<div className="p-4 text-center">
|
|
795
|
+
<h2 className="text-lg font-semibold text-red-600">
|
|
796
|
+
Algo salió mal
|
|
797
|
+
</h2>
|
|
798
|
+
<p className="text-gray-600">
|
|
799
|
+
Por favor, intenta recargar la página
|
|
800
|
+
</p>
|
|
801
|
+
</div>
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return this.props.children;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
### 11.2 Manejo de Errores en API
|
|
811
|
+
|
|
812
|
+
```typescript
|
|
813
|
+
// shared/lib/api-client.ts
|
|
814
|
+
import axios, { AxiosError } from 'axios';
|
|
815
|
+
|
|
816
|
+
const apiClient = axios.create({
|
|
817
|
+
baseURL: import.meta.env.VITE_API_URL,
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
apiClient.interceptors.response.use(
|
|
821
|
+
(response) => response,
|
|
822
|
+
(error: AxiosError<{ message: string }>) => {
|
|
823
|
+
const message = error.response?.data?.message || 'Error de conexión';
|
|
824
|
+
|
|
825
|
+
// Mensaje amigable para el usuario
|
|
826
|
+
return Promise.reject(new Error(message));
|
|
827
|
+
}
|
|
828
|
+
);
|
|
312
829
|
```
|
|
313
830
|
|
|
314
|
-
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
## 12. Progressive Web App
|
|
834
|
+
|
|
835
|
+
### 12.1 Features PWA
|
|
315
836
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
837
|
+
| Feature | Implementación |
|
|
838
|
+
|---------|----------------|
|
|
839
|
+
| Service Worker | Caching y soporte offline |
|
|
840
|
+
| Web App Manifest | Instalabilidad |
|
|
841
|
+
| Push Notifications | Cuando sea necesario |
|
|
842
|
+
| Background Sync | Sincronización al restaurar conexión |
|
|
321
843
|
|
|
322
|
-
###
|
|
323
|
-
- Centralized error handling in HTTP client
|
|
324
|
-
- Proper error typing
|
|
325
|
-
- User-friendly error messages
|
|
326
|
-
- Retry mechanisms for transient errors
|
|
844
|
+
### 12.2 Estrategia Offline
|
|
327
845
|
|
|
328
|
-
|
|
846
|
+
| Tipo de Recurso | Estrategia |
|
|
847
|
+
|-----------------|------------|
|
|
848
|
+
| Assets estáticos | Cache-first |
|
|
849
|
+
| Datos dinámicos | Network-first con fallback |
|
|
850
|
+
| Páginas offline | Fallback pre-cacheado |
|
|
851
|
+
| Formularios | Queue y sync cuando online |
|
|
329
852
|
|
|
330
|
-
|
|
331
|
-
- Service worker for caching and offline support
|
|
332
|
-
- Web app manifest for installability
|
|
333
|
-
- Push notifications (when needed)
|
|
334
|
-
- Background sync capabilities
|
|
853
|
+
---
|
|
335
854
|
|
|
336
|
-
|
|
337
|
-
- Cache-first for static assets
|
|
338
|
-
- Network-first for dynamic data
|
|
339
|
-
- Fallback pages for offline scenarios
|
|
340
|
-
- Sync when connection restored
|
|
855
|
+
## 13. Estándares de Idioma
|
|
341
856
|
|
|
342
|
-
|
|
857
|
+
### 13.1 Español para Todo Contenido Visible al Usuario
|
|
343
858
|
|
|
344
|
-
|
|
859
|
+
**REGLA CRÍTICA: Todo texto visible al usuario final DEBE estar en español.**
|
|
345
860
|
|
|
346
|
-
|
|
861
|
+
#### ✅ Español Requerido
|
|
347
862
|
|
|
348
|
-
|
|
349
|
-
-
|
|
350
|
-
-
|
|
351
|
-
- Any text the user sees (frontend or backend)
|
|
863
|
+
- Labels de UI, botones, formularios, mensajes, notificaciones
|
|
864
|
+
- Respuestas de API, errores de validación, templates de email
|
|
865
|
+
- Cualquier texto que el usuario vea (frontend o backend)
|
|
352
866
|
|
|
353
|
-
#### ✅
|
|
354
|
-
- Code (variables, functions, classes)
|
|
355
|
-
- Technical logs, comments, git commits
|
|
356
|
-
- Developer documentation
|
|
867
|
+
#### ✅ Inglés Requerido
|
|
357
868
|
|
|
358
|
-
|
|
869
|
+
- Código (variables, funciones, clases)
|
|
870
|
+
- Logs técnicos, comentarios, commits de git
|
|
871
|
+
- Documentación técnica para desarrolladores
|
|
872
|
+
|
|
873
|
+
#### ❌ Nunca Mezclar Idiomas en Texto Visible
|
|
359
874
|
|
|
360
|
-
**Examples:**
|
|
361
875
|
```typescript
|
|
362
|
-
// ✅
|
|
876
|
+
// ✅ CORRECTO
|
|
363
877
|
<Button>Guardar</Button>
|
|
364
878
|
toast.success("Datos guardados correctamente");
|
|
365
879
|
throw new BadRequestException('No se pudo crear el usuario');
|
|
366
880
|
|
|
367
|
-
// ❌
|
|
881
|
+
// ❌ INCORRECTO
|
|
368
882
|
<Button>Save cambios</Button>
|
|
369
883
|
toast.error("Failed al guardar");
|
|
370
884
|
throw new BadRequestException('Invalid datos proporcionados');
|
|
371
885
|
```
|
|
372
886
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
887
|
+
### 13.2 Implementación
|
|
888
|
+
|
|
889
|
+
```typescript
|
|
890
|
+
// shared/constants/messages.ts
|
|
891
|
+
export const MESSAGES = {
|
|
892
|
+
SUCCESS: {
|
|
893
|
+
SAVED: 'Datos guardados correctamente',
|
|
894
|
+
DELETED: 'Elemento eliminado correctamente',
|
|
895
|
+
UPDATED: 'Información actualizada',
|
|
896
|
+
},
|
|
897
|
+
ERROR: {
|
|
898
|
+
GENERIC: 'Ha ocurrido un error. Por favor, intenta de nuevo',
|
|
899
|
+
NOT_FOUND: 'El recurso solicitado no fue encontrado',
|
|
900
|
+
UNAUTHORIZED: 'No tienes permisos para realizar esta acción',
|
|
901
|
+
VALIDATION: 'Por favor, verifica los datos ingresados',
|
|
902
|
+
},
|
|
903
|
+
LOADING: {
|
|
904
|
+
DEFAULT: 'Cargando...',
|
|
905
|
+
SAVING: 'Guardando...',
|
|
906
|
+
PROCESSING: 'Procesando...',
|
|
907
|
+
},
|
|
908
|
+
} as const;
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
## 14. Consideraciones Generales
|
|
914
|
+
|
|
915
|
+
### 14.1 Gestor de Paquetes
|
|
916
|
+
|
|
917
|
+
**Regla:** Usar `pnpm` como gestor de paquetes por defecto, pero respetar el gestor existente en proyectos ya iniciados.
|
|
918
|
+
|
|
919
|
+
| Escenario | Acción | Razón |
|
|
920
|
+
|-----------|--------|-------|
|
|
921
|
+
| Proyecto nuevo | Usar `pnpm` | Mejor rendimiento y manejo de dependencias |
|
|
922
|
+
| Proyecto con `package-lock.json` | Continuar con `npm` | Evitar conflictos de lockfiles |
|
|
923
|
+
| Proyecto con `yarn.lock` | Continuar con `yarn` | Evitar conflictos de lockfiles |
|
|
924
|
+
| Proyecto con `pnpm-lock.yaml` | Continuar con `pnpm` | Ya está configurado |
|
|
925
|
+
|
|
926
|
+
#### Cómo Identificar el Gestor Actual
|
|
927
|
+
|
|
928
|
+
```bash
|
|
929
|
+
ls -la | grep -E "package-lock|yarn.lock|pnpm-lock"
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
| Archivo encontrado | Gestor a usar |
|
|
933
|
+
|--------------------|---------------|
|
|
934
|
+
| `package-lock.json` | `npm` |
|
|
935
|
+
| `yarn.lock` | `yarn` |
|
|
936
|
+
| `pnpm-lock.yaml` | `pnpm` |
|
|
937
|
+
| Ninguno | `pnpm` (proyecto nuevo) |
|
|
938
|
+
|
|
939
|
+
#### Comandos Equivalentes
|
|
940
|
+
|
|
941
|
+
| Acción | pnpm | npm | yarn |
|
|
942
|
+
|--------|------|-----|------|
|
|
943
|
+
| Instalar | `pnpm install` | `npm install` | `yarn` |
|
|
944
|
+
| Agregar dep | `pnpm add <pkg>` | `npm install <pkg>` | `yarn add <pkg>` |
|
|
945
|
+
| Agregar dev | `pnpm add -D <pkg>` | `npm install -D <pkg>` | `yarn add -D <pkg>` |
|
|
946
|
+
| Ejecutar script | `pnpm <script>` | `npm run <script>` | `yarn <script>` |
|
|
947
|
+
| Remover | `pnpm remove <pkg>` | `npm uninstall <pkg>` | `yarn remove <pkg>` |
|
|
948
|
+
|
|
949
|
+
> **⚠️ Importante:** Nunca mezclar gestores de paquetes en un mismo proyecto.
|
|
950
|
+
|
|
951
|
+
---
|
|
952
|
+
|
|
953
|
+
## 15. Configuración Base
|
|
954
|
+
|
|
955
|
+
### 15.1 `vite.config.ts`
|
|
956
|
+
|
|
957
|
+
```typescript
|
|
958
|
+
import { defineConfig } from 'vite';
|
|
959
|
+
import react from '@vitejs/plugin-react';
|
|
960
|
+
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
|
|
961
|
+
import viteTsConfigPaths from 'vite-tsconfig-paths';
|
|
962
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
963
|
+
|
|
964
|
+
export default defineConfig({
|
|
965
|
+
plugins: [
|
|
966
|
+
// TanStack Router DEBE ir primero
|
|
967
|
+
TanStackRouterVite({
|
|
968
|
+
target: 'react',
|
|
969
|
+
autoCodeSplitting: true,
|
|
970
|
+
routesDirectory: './src/routes',
|
|
971
|
+
generatedRouteTree: './src/routeTree.gen.ts',
|
|
972
|
+
routeFileIgnorePrefix: '-',
|
|
973
|
+
}),
|
|
974
|
+
react(),
|
|
975
|
+
viteTsConfigPaths(),
|
|
976
|
+
tailwindcss(),
|
|
977
|
+
],
|
|
978
|
+
resolve: {
|
|
979
|
+
alias: {
|
|
980
|
+
'@': '/src',
|
|
981
|
+
'@modules': '/src/modules',
|
|
982
|
+
'@shared': '/src/shared',
|
|
983
|
+
'@app': '/src/app',
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
server: {
|
|
987
|
+
port: 3000,
|
|
988
|
+
},
|
|
989
|
+
build: {
|
|
990
|
+
outDir: 'dist',
|
|
991
|
+
},
|
|
992
|
+
});
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
### 15.2 `src/router.tsx`
|
|
996
|
+
|
|
997
|
+
```typescript
|
|
998
|
+
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
|
|
999
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
1000
|
+
import { routerWithQueryClient } from '@tanstack/react-router-with-query';
|
|
1001
|
+
import { routeTree } from './routeTree.gen';
|
|
1002
|
+
|
|
1003
|
+
function NotFoundPage() {
|
|
1004
|
+
return (
|
|
1005
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
1006
|
+
<div className="text-center">
|
|
1007
|
+
<h1 className="text-6xl font-bold">404</h1>
|
|
1008
|
+
<p className="mt-4">Página no encontrada</p>
|
|
1009
|
+
</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function ErrorPage({ error }: { error: Error }) {
|
|
1015
|
+
return (
|
|
1016
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
1017
|
+
<div className="text-center">
|
|
1018
|
+
<h1 className="text-2xl font-bold text-red-600">Error</h1>
|
|
1019
|
+
<p className="mt-4">{error.message}</p>
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export function createRouter() {
|
|
1026
|
+
const queryClient = new QueryClient({
|
|
1027
|
+
defaultOptions: {
|
|
1028
|
+
queries: {
|
|
1029
|
+
staleTime: 60 * 1000,
|
|
1030
|
+
refetchOnWindowFocus: false,
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
const router = createTanStackRouter({
|
|
1036
|
+
routeTree,
|
|
1037
|
+
context: { queryClient },
|
|
1038
|
+
defaultPreload: 'intent',
|
|
1039
|
+
scrollRestoration: true,
|
|
1040
|
+
defaultNotFoundComponent: NotFoundPage,
|
|
1041
|
+
defaultErrorComponent: ErrorPage,
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
return routerWithQueryClient(router, queryClient);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
declare module '@tanstack/react-router' {
|
|
1048
|
+
interface Register {
|
|
1049
|
+
router: ReturnType<typeof createRouter>;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
### 15.3 `src/main.tsx`
|
|
1055
|
+
|
|
1056
|
+
```typescript
|
|
1057
|
+
import { StrictMode } from 'react';
|
|
1058
|
+
import { createRoot } from 'react-dom/client';
|
|
1059
|
+
import { RouterProvider } from '@tanstack/react-router';
|
|
1060
|
+
import { createRouter } from './router';
|
|
1061
|
+
import './globals.css';
|
|
1062
|
+
|
|
1063
|
+
const router = createRouter();
|
|
1064
|
+
|
|
1065
|
+
createRoot(document.getElementById('root')!).render(
|
|
1066
|
+
<StrictMode>
|
|
1067
|
+
<RouterProvider router={router} />
|
|
1068
|
+
</StrictMode>
|
|
1069
|
+
);
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
### 15.4 `src/routes/__root.tsx`
|
|
1073
|
+
|
|
1074
|
+
```typescript
|
|
1075
|
+
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
|
1076
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
1077
|
+
import { Toaster } from 'sonner';
|
|
1078
|
+
|
|
1079
|
+
interface RouterContext {
|
|
1080
|
+
queryClient: QueryClient;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
1084
|
+
component: RootComponent,
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
function RootComponent() {
|
|
1088
|
+
const { queryClient } = Route.useRouteContext();
|
|
1089
|
+
|
|
1090
|
+
return (
|
|
1091
|
+
<QueryClientProvider client={queryClient}>
|
|
1092
|
+
<Outlet />
|
|
1093
|
+
<Toaster position="bottom-right" />
|
|
1094
|
+
</QueryClientProvider>
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
### 15.5 `vitest.config.ts`
|
|
1100
|
+
|
|
1101
|
+
```typescript
|
|
1102
|
+
import { defineConfig } from 'vitest/config';
|
|
1103
|
+
import react from '@vitejs/plugin-react';
|
|
1104
|
+
import viteTsConfigPaths from 'vite-tsconfig-paths';
|
|
1105
|
+
|
|
1106
|
+
export default defineConfig({
|
|
1107
|
+
plugins: [react(), viteTsConfigPaths()],
|
|
1108
|
+
test: {
|
|
1109
|
+
globals: true,
|
|
1110
|
+
environment: 'jsdom',
|
|
1111
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
1112
|
+
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
|
1113
|
+
coverage: {
|
|
1114
|
+
provider: 'v8',
|
|
1115
|
+
reporter: ['text', 'json', 'html'],
|
|
1116
|
+
exclude: [
|
|
1117
|
+
'node_modules/',
|
|
1118
|
+
'src/test/',
|
|
1119
|
+
'**/*.d.ts',
|
|
1120
|
+
'src/routeTree.gen.ts',
|
|
1121
|
+
],
|
|
1122
|
+
},
|
|
1123
|
+
},
|
|
1124
|
+
});
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
---
|
|
1128
|
+
|
|
1129
|
+
## 16. Checklist de Implementación
|
|
1130
|
+
|
|
1131
|
+
### Configuración Inicial
|
|
1132
|
+
|
|
1133
|
+
- [ ] Instalar dependencias: `@tanstack/react-router`, `@tanstack/router-plugin`, `@tanstack/react-query`
|
|
1134
|
+
- [ ] Crear `vite.config.ts` con TanStackRouterVite plugin (PRIMERO)
|
|
1135
|
+
- [ ] Crear `src/router.tsx` con configuración del router
|
|
1136
|
+
- [ ] Crear `src/main.tsx` con RouterProvider
|
|
1137
|
+
- [ ] Crear `src/routes/__root.tsx`
|
|
1138
|
+
- [ ] Configurar path aliases en `tsconfig.json`
|
|
1139
|
+
- [ ] Agregar `routeTree.gen.ts` a `.prettierignore` y `.eslintignore`
|
|
1140
|
+
- [ ] Configurar Vitest
|
|
1141
|
+
|
|
1142
|
+
### Estructura de Rutas
|
|
1143
|
+
|
|
1144
|
+
- [ ] Crear layouts pathless (`_auth.tsx`, `_app.tsx`)
|
|
1145
|
+
- [ ] Implementar guards de autenticación en `beforeLoad`
|
|
1146
|
+
- [ ] Usar `$param` para rutas dinámicas
|
|
1147
|
+
- [ ] Usar `-` para carpetas de colocación
|
|
1148
|
+
- [ ] Verificar que las URLs sean correctas
|
|
1149
|
+
|
|
1150
|
+
### Arquitectura
|
|
1151
|
+
|
|
1152
|
+
- [ ] Organizar módulos siguiendo Module/Domain/Feature
|
|
1153
|
+
- [ ] Implementar capas de Clean Architecture por feature
|
|
1154
|
+
- [ ] Configurar stores Zustand por feature
|
|
1155
|
+
- [ ] Configurar TanStack Query para server state
|
|
1156
|
+
- [ ] Crear barrel exports (`index.ts`) por feature
|
|
1157
|
+
|
|
1158
|
+
### Calidad
|
|
1159
|
+
|
|
1160
|
+
- [ ] Configurar Vitest con coverage
|
|
1161
|
+
- [ ] Agregar tests unitarios para use cases
|
|
1162
|
+
- [ ] Agregar tests de componentes
|
|
1163
|
+
- [ ] Verificar accesibilidad (axe)
|
|
1164
|
+
- [ ] Validar textos en español
|
|
1165
|
+
|
|
1166
|
+
---
|
|
1167
|
+
|
|
1168
|
+
## Referencias
|
|
1169
|
+
|
|
1170
|
+
- [TanStack Router Documentation](https://tanstack.com/router/latest)
|
|
1171
|
+
- [TanStack Query Documentation](https://tanstack.com/query/latest)
|
|
1172
|
+
- [Zustand Documentation](https://zustand-demo.pmnd.rs/)
|
|
1173
|
+
- [Vite Documentation](https://vitejs.dev/)
|
|
1174
|
+
- [Vitest Documentation](https://vitest.dev/)
|
|
1175
|
+
- [Clean Architecture - Robert C. Martin](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|