create-bunspace 0.3.0 → 0.4.0
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.
- package/dist/bin.js +237 -34
- package/dist/templates/fumadocs/MUST-FOLLOW-GUIDELINES.md +1496 -33
- package/dist/templates/monorepo/MUST-FOLLOW-GUIDELINES.md +1496 -33
- package/dist/templates/monorepo/apps/example/package.json +1 -1
- package/dist/templates/monorepo/core/packages/utils/rolldown.config.ts +30 -0
- package/dist/templates/monorepo/tsconfig.json +3 -1
- package/dist/templates/telegram-bot/MUST-FOLLOW-GUIDELINES.md +1732 -0
- package/dist/templates/telegram-bot/bun.lock +6 -20
- package/dist/templates/telegram-bot/core/package.json +11 -0
- package/dist/templates/telegram-bot/core/rolldown.config.ts +11 -0
- package/dist/templates/telegram-bot/core/src/config/logging.ts +1 -3
- package/dist/templates/telegram-bot/core/src/handlers/config-export.ts +10 -9
- package/dist/templates/telegram-bot/core/src/handlers/control.ts +30 -21
- package/dist/templates/telegram-bot/core/src/handlers/demo-full.ts +58 -0
- package/dist/templates/telegram-bot/core/src/handlers/demo-keyboard.ts +49 -0
- package/dist/templates/telegram-bot/core/src/handlers/demo-media.ts +163 -0
- package/dist/templates/telegram-bot/core/src/handlers/demo-text.ts +27 -0
- package/dist/templates/telegram-bot/core/src/handlers/health.ts +22 -14
- package/dist/templates/telegram-bot/core/src/handlers/info.ts +21 -23
- package/dist/templates/telegram-bot/core/src/handlers/logs.ts +8 -6
- package/dist/templates/telegram-bot/core/src/index.ts +20 -1
- package/dist/templates/telegram-bot/core/src/utils/formatters.ts +5 -60
- package/dist/templates/telegram-bot/core/tsconfig.json +2 -0
- package/dist/templates/telegram-bot/package.json +3 -2
- package/dist/templates/telegram-bot/packages/utils/package.json +12 -1
- package/dist/templates/telegram-bot/packages/utils/rolldown.config.ts +11 -0
- package/dist/templates/telegram-bot/packages/utils/tsconfig.json +10 -0
- package/dist/templates/telegram-bot/tsconfig.json +7 -2
- package/package.json +6 -3
- package/templates/fumadocs/MUST-FOLLOW-GUIDELINES.md +1496 -33
- package/templates/monorepo/MUST-FOLLOW-GUIDELINES.md +1496 -33
- package/templates/monorepo/apps/example/package.json +1 -1
- package/templates/monorepo/core/packages/utils/rolldown.config.ts +30 -0
- package/templates/monorepo/tsconfig.json +3 -1
- package/templates/telegram-bot/MUST-FOLLOW-GUIDELINES.md +1732 -0
- package/templates/telegram-bot/bun.lock +6 -20
- package/templates/telegram-bot/core/package.json +11 -0
- package/templates/telegram-bot/core/rolldown.config.ts +11 -0
- package/templates/telegram-bot/core/src/config/logging.ts +1 -3
- package/templates/telegram-bot/core/src/handlers/config-export.ts +10 -9
- package/templates/telegram-bot/core/src/handlers/control.ts +30 -21
- package/templates/telegram-bot/core/src/handlers/demo-full.ts +58 -0
- package/templates/telegram-bot/core/src/handlers/demo-keyboard.ts +49 -0
- package/templates/telegram-bot/core/src/handlers/demo-media.ts +163 -0
- package/templates/telegram-bot/core/src/handlers/demo-text.ts +27 -0
- package/templates/telegram-bot/core/src/handlers/health.ts +22 -14
- package/templates/telegram-bot/core/src/handlers/info.ts +21 -23
- package/templates/telegram-bot/core/src/handlers/logs.ts +8 -6
- package/templates/telegram-bot/core/src/index.ts +20 -1
- package/templates/telegram-bot/core/src/utils/formatters.ts +5 -60
- package/templates/telegram-bot/core/tsconfig.json +2 -0
- package/templates/telegram-bot/package.json +3 -2
- package/templates/telegram-bot/packages/utils/package.json +12 -1
- package/templates/telegram-bot/packages/utils/rolldown.config.ts +11 -0
- package/templates/telegram-bot/packages/utils/tsconfig.json +10 -0
- package/templates/telegram-bot/tsconfig.json +7 -2
- package/templates/telegram-bot/core/src/utils/message-builder.ts +0 -180
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
### Root del Monorepo
|
|
27
27
|
```
|
|
28
|
-
|
|
28
|
+
mks-dev-environment/
|
|
29
29
|
├── docs/ # Documentacion del proyecto
|
|
30
30
|
├── tools/ # Scripts y herramientas de desarrollo
|
|
31
31
|
├── core/
|
|
@@ -105,29 +105,248 @@ export async function myFunction(
|
|
|
105
105
|
|
|
106
106
|
## REGLA 2: Logging - NUNCA console.log
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
Usar `@mks2508/better-logger` para todo el logging.
|
|
109
|
+
|
|
110
|
+
### Imports y Setup Basico
|
|
109
111
|
|
|
110
112
|
```typescript
|
|
111
|
-
|
|
113
|
+
// Singleton (recomendado para la mayoria de casos)
|
|
114
|
+
import logger from '@mks2508/better-logger';
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
// O crear instancia personalizada
|
|
117
|
+
import { Logger } from '@mks2508/better-logger';
|
|
118
|
+
const log = new Logger({
|
|
119
|
+
verbosity: 'debug', // 'debug' | 'info' | 'warn' | 'error' | 'silent'
|
|
120
|
+
enableStackTrace: true, // Muestra archivo:linea
|
|
121
|
+
bufferSize: 1000, // Para exportacion de logs
|
|
122
|
+
});
|
|
123
|
+
```
|
|
114
124
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
### Metodos de Logging
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// Niveles basicos
|
|
129
|
+
logger.debug('Debug message', { context });
|
|
130
|
+
logger.info('Info message');
|
|
131
|
+
logger.warn('Warning message');
|
|
132
|
+
logger.error('Error message', errorObject);
|
|
133
|
+
logger.success('Operation completed');
|
|
134
|
+
logger.critical('System failure!');
|
|
135
|
+
logger.trace('Trace from nested function');
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Scoped Loggers (USAR SIEMPRE en servicios)
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// ComponentLogger - Para componentes UI y servicios
|
|
142
|
+
const authLog = logger.component('AuthService');
|
|
143
|
+
authLog.info('Usuario autenticando...'); // [COMPONENT] [AuthService] Usuario...
|
|
144
|
+
authLog.success('Login exitoso');
|
|
145
|
+
authLog.lifecycle('mount', 'Component mounted');
|
|
146
|
+
authLog.stateChange('idle', 'loading');
|
|
147
|
+
|
|
148
|
+
// APILogger - Para endpoints y llamadas HTTP
|
|
149
|
+
const apiLog = logger.api('UserAPI');
|
|
150
|
+
apiLog.info('GET /users'); // [API] [UserAPI] GET /users
|
|
151
|
+
apiLog.slow('Response slow', 2500); // [API] [SLOW] Response slow (2500ms)
|
|
152
|
+
apiLog.rateLimit('Too many requests'); // [API] [RATE_LIMIT]
|
|
153
|
+
apiLog.deprecated('Use /v2/users instead');
|
|
154
|
+
|
|
155
|
+
// ScopedLogger - Generico con contextos anidados
|
|
156
|
+
const dbLog = logger.scope('Database');
|
|
157
|
+
dbLog.info('Query executing');
|
|
158
|
+
dbLog.context('transactions').run(() => {
|
|
159
|
+
dbLog.info('Inside transaction'); // [Database:transactions] Inside...
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Timing y Performance
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Medir operaciones
|
|
167
|
+
logger.time('db-query');
|
|
168
|
+
await db.query('SELECT * FROM users');
|
|
169
|
+
logger.timeEnd('db-query'); // Timer: db-query - 234.56ms
|
|
170
|
+
|
|
171
|
+
// En scoped loggers
|
|
172
|
+
const serviceLog = logger.component('ProductService');
|
|
173
|
+
serviceLog.time('fetch-products');
|
|
174
|
+
const products = await fetchProducts();
|
|
175
|
+
serviceLog.timeEnd('fetch-products');
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Badges para Contexto
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// Badges encadenables
|
|
182
|
+
logger.badges(['CACHE', 'HIT']).info('Data from cache');
|
|
183
|
+
logger.badge('v2').badge('stable').info('API response');
|
|
184
|
+
|
|
185
|
+
// En scoped loggers
|
|
186
|
+
const api = logger.api('GraphQL');
|
|
187
|
+
api.badges(['mutation', 'user']).info('createUser executed');
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Transports (Envio de Logs)
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import logger, {
|
|
194
|
+
FileTransport,
|
|
195
|
+
HttpTransport,
|
|
196
|
+
addTransport
|
|
197
|
+
} from '@mks2508/better-logger';
|
|
198
|
+
|
|
199
|
+
// Transport a archivo (solo Node.js/Bun)
|
|
200
|
+
logger.addTransport({
|
|
201
|
+
target: 'file',
|
|
202
|
+
options: {
|
|
203
|
+
destination: '/var/log/app.log',
|
|
204
|
+
batchSize: 100,
|
|
205
|
+
flushInterval: 5000 // ms
|
|
206
|
+
},
|
|
207
|
+
level: 'warn' // Solo warn+ van al archivo
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Transport HTTP (envia a servidor de logs)
|
|
211
|
+
logger.addTransport({
|
|
212
|
+
target: 'http',
|
|
213
|
+
options: {
|
|
214
|
+
url: 'https://logs.example.com/ingest',
|
|
215
|
+
headers: { 'Authorization': 'Bearer xxx' },
|
|
216
|
+
batchSize: 50,
|
|
217
|
+
flushInterval: 10000
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Flush manual antes de cerrar
|
|
222
|
+
await logger.flushTransports();
|
|
223
|
+
await logger.closeTransports();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Hooks y Middleware
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Hook beforeLog - agregar metadata
|
|
230
|
+
logger.on('beforeLog', (entry) => {
|
|
231
|
+
entry.correlationId = getCorrelationId();
|
|
232
|
+
entry.userId = getCurrentUserId();
|
|
233
|
+
return entry;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Hook afterLog - side effects
|
|
237
|
+
logger.on('afterLog', (entry) => {
|
|
238
|
+
if (entry.level === 'error') {
|
|
239
|
+
sendToErrorTracking(entry);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Middleware - pipeline de procesamiento
|
|
244
|
+
logger.use((entry, next) => {
|
|
245
|
+
// Enriquecer con request context
|
|
246
|
+
const store = asyncLocalStorage.getStore();
|
|
247
|
+
if (store?.requestId) {
|
|
248
|
+
entry.requestId = store.requestId;
|
|
249
|
+
}
|
|
250
|
+
next();
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Serializers Personalizados
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Serializar errores de forma estructurada
|
|
258
|
+
logger.addSerializer(Error, (err) => ({
|
|
259
|
+
name: err.name,
|
|
260
|
+
message: err.message,
|
|
261
|
+
stack: err.stack?.split('\n').slice(0, 5),
|
|
262
|
+
code: (err as any).code
|
|
263
|
+
}));
|
|
264
|
+
|
|
265
|
+
// Serializar objetos custom
|
|
266
|
+
logger.addSerializer(User, (user) => ({
|
|
267
|
+
id: user.id,
|
|
268
|
+
email: '[REDACTED]',
|
|
269
|
+
role: user.role
|
|
270
|
+
}));
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Configuracion Frontend vs Backend
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// === BACKEND (Node.js/Bun) ===
|
|
277
|
+
import logger from '@mks2508/better-logger';
|
|
278
|
+
|
|
279
|
+
// Preset optimizado para terminal
|
|
280
|
+
logger.preset('cyberpunk');
|
|
281
|
+
logger.showTimestamp();
|
|
282
|
+
logger.showLocation();
|
|
283
|
+
|
|
284
|
+
// Transport a archivo
|
|
285
|
+
logger.addTransport({
|
|
286
|
+
target: 'file',
|
|
287
|
+
options: { destination: './logs/app.log' }
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// === FRONTEND (Browser) ===
|
|
291
|
+
import logger from '@mks2508/better-logger';
|
|
292
|
+
|
|
293
|
+
// Preset con colores CSS
|
|
294
|
+
logger.preset('default');
|
|
295
|
+
logger.hideLocation(); // No util en browser
|
|
296
|
+
|
|
297
|
+
// Transport HTTP para enviar errores
|
|
298
|
+
logger.addTransport({
|
|
299
|
+
target: 'http',
|
|
300
|
+
options: { url: '/api/logs' },
|
|
301
|
+
level: 'error' // Solo errores al servidor
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Verbosity y Filtrado
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// Cambiar nivel de verbosidad
|
|
309
|
+
logger.setVerbosity('warn'); // Solo warn, error, critical
|
|
310
|
+
logger.setVerbosity('silent'); // Desactiva todo
|
|
311
|
+
logger.setVerbosity('debug'); // Muestra todo
|
|
312
|
+
|
|
313
|
+
// Configuracion condicional
|
|
314
|
+
if (process.env.NODE_ENV === 'production') {
|
|
315
|
+
logger.setVerbosity('warn');
|
|
316
|
+
logger.hideLocation();
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Utilidades de Grupos y Tablas
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// Tablas de datos
|
|
324
|
+
logger.table([
|
|
325
|
+
{ name: 'Alice', age: 30 },
|
|
326
|
+
{ name: 'Bob', age: 25 }
|
|
327
|
+
]);
|
|
328
|
+
|
|
329
|
+
// Grupos colapsables
|
|
330
|
+
logger.group('Database Operations');
|
|
331
|
+
logger.info('Connecting...');
|
|
332
|
+
logger.info('Querying...');
|
|
333
|
+
logger.groupEnd();
|
|
334
|
+
|
|
335
|
+
// Grupo colapsado por defecto
|
|
336
|
+
logger.group('Debug Details', true);
|
|
337
|
+
logger.debug('Verbose info here');
|
|
338
|
+
logger.groupEnd();
|
|
121
339
|
```
|
|
122
340
|
|
|
123
341
|
### Prohibido
|
|
124
342
|
|
|
125
343
|
```typescript
|
|
126
|
-
// INCORRECTO
|
|
344
|
+
// INCORRECTO - NUNCA usar
|
|
127
345
|
console.log('Started');
|
|
128
346
|
console.error('Failed');
|
|
129
347
|
console.info('Info');
|
|
130
348
|
console.warn('Warning');
|
|
349
|
+
console.debug('Debug');
|
|
131
350
|
```
|
|
132
351
|
|
|
133
352
|
---
|
|
@@ -136,43 +355,212 @@ console.warn('Warning');
|
|
|
136
355
|
|
|
137
356
|
### Obligatorio
|
|
138
357
|
|
|
139
|
-
TODA operacion que pueda fallar DEBE usar `Result<T, E>` del package
|
|
358
|
+
TODA operacion que pueda fallar DEBE usar `Result<T, E>` del package `@mks2508/no-throw`:
|
|
140
359
|
|
|
141
360
|
```typescript
|
|
142
361
|
import {
|
|
143
|
-
ok,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
362
|
+
ok, err, fail,
|
|
363
|
+
isOk, isErr,
|
|
364
|
+
map, mapErr, flatMap,
|
|
365
|
+
match,
|
|
366
|
+
tryCatch, tryCatchAsync, fromPromise,
|
|
367
|
+
unwrap, unwrapOr, unwrapOrElse,
|
|
368
|
+
tap, tapErr,
|
|
369
|
+
collect, all,
|
|
370
|
+
type Result, type ResultError
|
|
371
|
+
} from '@mks2508/no-throw';
|
|
372
|
+
```
|
|
151
373
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
374
|
+
### Constructores
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// Crear resultado exitoso
|
|
378
|
+
const success = ok(42); // Result<number, never>
|
|
379
|
+
const success2 = ok({ name: 'John' }); // Result<{name: string}, never>
|
|
380
|
+
|
|
381
|
+
// Crear resultado de error
|
|
382
|
+
const error = err('Something failed'); // Result<never, string>
|
|
383
|
+
|
|
384
|
+
// Crear error estructurado con fail()
|
|
385
|
+
const structuredError = fail(
|
|
386
|
+
'NETWORK_ERROR', // code
|
|
387
|
+
'Failed to fetch data', // message
|
|
388
|
+
originalError // cause (opcional)
|
|
389
|
+
);
|
|
390
|
+
// Retorna: Result<never, ResultError<'NETWORK_ERROR'>>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Type Guards
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
const result = await fetchData(url);
|
|
397
|
+
|
|
398
|
+
if (isOk(result)) {
|
|
399
|
+
console.log(result.value); // Tipo: T
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (isErr(result)) {
|
|
403
|
+
console.log(result.error); // Tipo: E
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Transformaciones
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
// map - transforma el valor si es Ok
|
|
411
|
+
const doubled = map(result, (n) => n * 2);
|
|
412
|
+
|
|
413
|
+
// mapErr - transforma el error si es Err
|
|
414
|
+
const mappedErr = mapErr(result, (e) => ({ ...e, timestamp: Date.now() }));
|
|
415
|
+
|
|
416
|
+
// flatMap - encadena operaciones que retornan Result
|
|
417
|
+
const chained = flatMap(result, (value) => {
|
|
418
|
+
if (value > 100) return err('Too large');
|
|
419
|
+
return ok(value * 2);
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Pattern Matching
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
const message = match(result, {
|
|
427
|
+
ok: (value) => `Success: ${value}`,
|
|
428
|
+
err: (error) => `Error: ${error.message}`
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Manejo de Excepciones
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// tryCatch - para operaciones sincronas
|
|
436
|
+
const syncResult = tryCatch(
|
|
437
|
+
() => JSON.parse(jsonString),
|
|
438
|
+
'PARSE_ERROR'
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// tryCatchAsync - para operaciones async
|
|
442
|
+
const asyncResult = await tryCatchAsync(
|
|
443
|
+
async () => {
|
|
444
|
+
const response = await fetch(url);
|
|
445
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
446
|
+
return response.json();
|
|
447
|
+
},
|
|
448
|
+
'NETWORK_ERROR'
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// fromPromise - convierte Promise a Result
|
|
452
|
+
const promiseResult = await fromPromise(
|
|
453
|
+
fetch(url).then(r => r.json()),
|
|
454
|
+
'FETCH_ERROR'
|
|
455
|
+
);
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Unwrap
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// unwrap - obtiene valor o lanza error
|
|
462
|
+
const value = unwrap(result); // Throws si es Err
|
|
463
|
+
|
|
464
|
+
// unwrapOr - valor por defecto
|
|
465
|
+
const valueOrDefault = unwrapOr(result, 0);
|
|
466
|
+
|
|
467
|
+
// unwrapOrElse - valor calculado
|
|
468
|
+
const valueOrComputed = unwrapOrElse(result, (error) => {
|
|
469
|
+
log.error('Using fallback due to:', error);
|
|
470
|
+
return defaultValue;
|
|
471
|
+
});
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Efectos Secundarios
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// tap - ejecuta efecto si es Ok (no modifica resultado)
|
|
478
|
+
const logged = tap(result, (value) => {
|
|
479
|
+
log.info('Got value:', value);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// tapErr - ejecuta efecto si es Err
|
|
483
|
+
const errorLogged = tapErr(result, (error) => {
|
|
484
|
+
log.error('Operation failed:', error);
|
|
485
|
+
});
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Colecciones
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
// collect - convierte array de Results en Result de array
|
|
492
|
+
const results: Result<number, string>[] = [ok(1), ok(2), ok(3)];
|
|
493
|
+
const collected = collect(results); // Result<number[], string>
|
|
494
|
+
|
|
495
|
+
// all - igual que collect (alias)
|
|
496
|
+
const allResults = all([ok(1), ok(2), ok(3)]);
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Ejemplo Completo
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import { ok, fail, isErr, tryCatchAsync, match } from '@mks2508/no-throw';
|
|
503
|
+
import logger from '@mks2508/better-logger';
|
|
504
|
+
|
|
505
|
+
const log = logger.component('UserService');
|
|
506
|
+
|
|
507
|
+
async function fetchUser(
|
|
508
|
+
id: string
|
|
509
|
+
): Promise<Result<IUser, ResultError<'NOT_FOUND' | 'NETWORK_ERROR'>>> {
|
|
510
|
+
const result = await tryCatchAsync(
|
|
156
511
|
async () => {
|
|
157
|
-
const response = await fetch(
|
|
512
|
+
const response = await fetch(`/api/users/${id}`);
|
|
513
|
+
if (response.status === 404) {
|
|
514
|
+
throw { code: 'NOT_FOUND' };
|
|
515
|
+
}
|
|
158
516
|
if (!response.ok) {
|
|
159
517
|
throw new Error(`HTTP ${response.status}`);
|
|
160
518
|
}
|
|
161
|
-
return await response.
|
|
519
|
+
return await response.json();
|
|
162
520
|
},
|
|
163
|
-
|
|
521
|
+
'NETWORK_ERROR'
|
|
164
522
|
);
|
|
165
523
|
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
);
|
|
524
|
+
if (isErr(result)) {
|
|
525
|
+
const error = result.error;
|
|
526
|
+
if (error.cause?.code === 'NOT_FOUND') {
|
|
527
|
+
return fail('NOT_FOUND', `User ${id} not found`);
|
|
528
|
+
}
|
|
529
|
+
return fail('NETWORK_ERROR', `Failed to fetch user ${id}`, error);
|
|
172
530
|
}
|
|
173
531
|
|
|
174
532
|
return ok(result.value);
|
|
175
533
|
}
|
|
534
|
+
|
|
535
|
+
// Uso
|
|
536
|
+
const userResult = await fetchUser('123');
|
|
537
|
+
const message = match(userResult, {
|
|
538
|
+
ok: (user) => {
|
|
539
|
+
log.success(`User loaded: ${user.name}`);
|
|
540
|
+
return user;
|
|
541
|
+
},
|
|
542
|
+
err: (error) => {
|
|
543
|
+
log.error(`Failed: ${error.message}`);
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Prohibido
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
// INCORRECTO - NUNCA usar try/catch directo sin Result
|
|
553
|
+
try {
|
|
554
|
+
const data = await fetchData();
|
|
555
|
+
} catch (e) {
|
|
556
|
+
console.error(e);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// INCORRECTO - NUNCA lanzar excepciones
|
|
560
|
+
throw new Error('Something failed');
|
|
561
|
+
|
|
562
|
+
// CORRECTO - Siempre retornar Result
|
|
563
|
+
return fail('ERROR_CODE', 'Description', cause);
|
|
176
564
|
```
|
|
177
565
|
|
|
178
566
|
---
|
|
@@ -259,11 +647,1086 @@ Antes de hacer commit de codigo, verificar:
|
|
|
259
647
|
|
|
260
648
|
---
|
|
261
649
|
|
|
650
|
+
## REGLA 7: TransactionState - Estados de UI
|
|
651
|
+
|
|
652
|
+
### Estados Soportados
|
|
653
|
+
|
|
654
|
+
| Estado | Uso |
|
|
655
|
+
|--------|-----|
|
|
656
|
+
| `initial` | Estado inicial, sin datos |
|
|
657
|
+
| `loading` | Primera carga en progreso |
|
|
658
|
+
| `revalidating` | Recargando con datos previos |
|
|
659
|
+
| `success` | Operacion completada con exito |
|
|
660
|
+
| `failed` | Error en la operacion |
|
|
661
|
+
|
|
662
|
+
### Factories y Guards
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
// Factories
|
|
666
|
+
createInitialState()
|
|
667
|
+
createLoadingState(message?: string)
|
|
668
|
+
createRevalidatingState(previousData: T)
|
|
669
|
+
createSuccessState(data: T, message?: string)
|
|
670
|
+
createFailedState(error: E)
|
|
671
|
+
|
|
672
|
+
// Guards
|
|
673
|
+
isInitial(state) // true si initial
|
|
674
|
+
isLoading(state) // true si loading
|
|
675
|
+
isRevalidating(state)
|
|
676
|
+
isSuccess(state)
|
|
677
|
+
isFailed(state)
|
|
678
|
+
isPending(state) // loading OR revalidating
|
|
679
|
+
isCompleted(state) // success OR failed
|
|
680
|
+
hasData(state) // success OR revalidating
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Patron en Hooks
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
const [state, setState] = useState<TransactionState<T, E>>(createInitialState);
|
|
687
|
+
|
|
688
|
+
const load = useCallback(async () => {
|
|
689
|
+
setState(createLoadingState('Cargando...'));
|
|
690
|
+
const result = await service.fetch();
|
|
691
|
+
if (isSuccess(result)) {
|
|
692
|
+
setState(createSuccessState(result.data));
|
|
693
|
+
} else {
|
|
694
|
+
setState(createFailedState(result.error));
|
|
695
|
+
}
|
|
696
|
+
}, []);
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## REGLA 8: Arquitectura BLO (Business Logic Layer)
|
|
702
|
+
|
|
703
|
+
### Principios Fundamentales
|
|
704
|
+
|
|
705
|
+
| Principio | Descripcion |
|
|
706
|
+
|-----------|-------------|
|
|
707
|
+
| **Separacion clara** | UI y logica de negocio en capas distintas |
|
|
708
|
+
| **Handlers** | Clases puras TypeScript sin dependencias React |
|
|
709
|
+
| **Hooks** | Puente reactivo entre handlers y componentes |
|
|
710
|
+
| **Componentes** | Solo UI y eventos, sin logica de negocio |
|
|
711
|
+
|
|
712
|
+
### Clasificacion de Componentes
|
|
713
|
+
|
|
714
|
+
#### Componentes UI Reutilizables (`components/ui/`)
|
|
715
|
+
- **Alta reusabilidad** en diferentes contextos
|
|
716
|
+
- **Baja complejidad** y focalizacion especifica
|
|
717
|
+
- **Estructura simplificada** (archivo unico o pocos archivos)
|
|
718
|
+
- Ejemplos: Button, Input, Card, Modal
|
|
719
|
+
|
|
720
|
+
#### Componentes de Dominio (`components/`)
|
|
721
|
+
- **Alta complejidad** y logica de negocio especifica
|
|
722
|
+
- **Estructura completa** con handler y hook
|
|
723
|
+
- **Subcomponentes** si es necesario
|
|
724
|
+
- Ejemplos: ProductCard, UserProfile, OrderDetails
|
|
725
|
+
|
|
726
|
+
### Estructura Completa (Componentes Complejos)
|
|
727
|
+
|
|
728
|
+
```
|
|
729
|
+
components/
|
|
730
|
+
└── ProductCard/
|
|
731
|
+
├── index.tsx # Componente React puro
|
|
732
|
+
├── ProductCard.types.ts # Interfaces y tipos
|
|
733
|
+
├── ProductCard.styles.ts # Clases Tailwind con CVA
|
|
734
|
+
├── ProductCard.handler.ts # Logica de negocio (BLO)
|
|
735
|
+
├── ProductCard.hook.ts # Hook React con estado
|
|
736
|
+
└── components/ # Subcomponentes (opcional)
|
|
737
|
+
├── ProductImage/
|
|
738
|
+
└── ProductActions/
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### Componente React (`index.tsx`)
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
import React from 'react';
|
|
745
|
+
import { IProductCardProps } from './ProductCard.types';
|
|
746
|
+
import { styles } from './ProductCard.styles';
|
|
747
|
+
import { useProductCard } from './ProductCard.hook';
|
|
748
|
+
|
|
749
|
+
export const ProductCard: React.FC<IProductCardProps> = (props) => {
|
|
750
|
+
const { state, actions } = useProductCard(props);
|
|
751
|
+
|
|
752
|
+
return (
|
|
753
|
+
<div className={styles.container}>
|
|
754
|
+
{/* Solo UI y eventos - sin logica de negocio */}
|
|
755
|
+
<h3 className={styles.title}>{state.data?.name}</h3>
|
|
756
|
+
<button onClick={actions.addToCart}>Agregar</button>
|
|
757
|
+
</div>
|
|
758
|
+
);
|
|
759
|
+
};
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
**Reglas del componente:**
|
|
763
|
+
- ✅ Solo UI: renderizado y eventos
|
|
764
|
+
- ✅ Props inmutables: no modificar props directamente
|
|
765
|
+
- ✅ Estado delegado: usar hook para estado y acciones
|
|
766
|
+
- ❌ Sin logica: no business logic en el componente
|
|
767
|
+
|
|
768
|
+
### Tipos TypeScript (`.types.ts`)
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
export interface IProductCardProps {
|
|
772
|
+
productId: string;
|
|
773
|
+
initialData?: IProduct;
|
|
774
|
+
autoLoad?: boolean;
|
|
775
|
+
className?: string;
|
|
776
|
+
onAddToCart?: (id: string) => void;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export interface IProductCardState {
|
|
780
|
+
isLoading: boolean;
|
|
781
|
+
data: IProduct | null;
|
|
782
|
+
error: string | null;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export interface IProductCardActions {
|
|
786
|
+
loadProduct: () => Promise<void>;
|
|
787
|
+
addToCart: () => void;
|
|
788
|
+
reset: () => void;
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### Estilos Tailwind con CVA (`.styles.ts`)
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
import { cva } from 'class-variance-authority';
|
|
796
|
+
|
|
797
|
+
export const containerVariants = cva(
|
|
798
|
+
"w-full p-4 bg-white border rounded-lg transition-shadow",
|
|
799
|
+
{
|
|
800
|
+
variants: {
|
|
801
|
+
variant: {
|
|
802
|
+
default: "border-gray-200 hover:shadow-md",
|
|
803
|
+
featured: "border-blue-200 bg-blue-50 hover:shadow-lg",
|
|
804
|
+
compact: "p-2 border-gray-100",
|
|
805
|
+
},
|
|
806
|
+
size: {
|
|
807
|
+
sm: "max-w-xs",
|
|
808
|
+
md: "max-w-sm",
|
|
809
|
+
lg: "max-w-md",
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
defaultVariants: {
|
|
813
|
+
variant: "default",
|
|
814
|
+
size: "md",
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
export const styles = {
|
|
820
|
+
container: containerVariants,
|
|
821
|
+
header: "flex items-center justify-between mb-4",
|
|
822
|
+
title: "text-lg font-semibold text-gray-900",
|
|
823
|
+
content: "text-gray-600 min-h-[100px]",
|
|
824
|
+
actions: "flex gap-2 mt-4 pt-4 border-t border-gray-200",
|
|
825
|
+
loading: "flex items-center justify-center py-8 text-gray-500",
|
|
826
|
+
error: "flex items-center justify-center py-8 text-red-500 bg-red-50 rounded",
|
|
827
|
+
};
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### Handler BLO (`.handler.ts`)
|
|
831
|
+
|
|
832
|
+
```typescript
|
|
833
|
+
import { IProductCardState } from './ProductCard.types';
|
|
834
|
+
|
|
835
|
+
export class ProductCardHandler {
|
|
836
|
+
private state: IProductCardState = {
|
|
837
|
+
isLoading: false,
|
|
838
|
+
data: null,
|
|
839
|
+
error: null,
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
constructor(initialData?: IProduct) {
|
|
843
|
+
if (initialData) {
|
|
844
|
+
this.state.data = initialData;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
getState(): IProductCardState {
|
|
849
|
+
return { ...this.state }; // Siempre retornar copia
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async loadProduct(productId: string): Promise<void> {
|
|
853
|
+
this.state.isLoading = true;
|
|
854
|
+
this.state.error = null;
|
|
855
|
+
|
|
856
|
+
try {
|
|
857
|
+
// Logica de negocio pura - sin React
|
|
858
|
+
const response = await fetch(`/api/products/${productId}`);
|
|
859
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
860
|
+
this.state.data = await response.json();
|
|
861
|
+
} catch (error) {
|
|
862
|
+
this.state.error = error instanceof Error ? error.message : 'Error';
|
|
863
|
+
} finally {
|
|
864
|
+
this.state.isLoading = false;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
reset(): void {
|
|
869
|
+
this.state = { isLoading: false, data: null, error: null };
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
**Reglas del handler:**
|
|
875
|
+
- ✅ Clase pura TypeScript: sin dependencias de React
|
|
876
|
+
- ✅ Estado inmutable: siempre retornar copias
|
|
877
|
+
- ✅ Logica de negocio: validaciones, transformaciones, API calls
|
|
878
|
+
- ✅ Testing facil: sin dependencias externas
|
|
879
|
+
- ❌ Sin React: no hooks, no JSX, no estado React
|
|
880
|
+
|
|
881
|
+
### Hook React (`.hook.ts`)
|
|
882
|
+
|
|
883
|
+
```typescript
|
|
884
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
885
|
+
import { IProductCardProps, IProductCardState, IProductCardActions } from './ProductCard.types';
|
|
886
|
+
import { ProductCardHandler } from './ProductCard.handler';
|
|
887
|
+
|
|
888
|
+
export const useProductCard = (props: IProductCardProps) => {
|
|
889
|
+
const [handler] = useState(() => new ProductCardHandler(props.initialData));
|
|
890
|
+
const [state, setState] = useState<IProductCardState>(handler.getState());
|
|
891
|
+
|
|
892
|
+
const loadProduct = useCallback(async () => {
|
|
893
|
+
await handler.loadProduct(props.productId);
|
|
894
|
+
setState(handler.getState());
|
|
895
|
+
}, [handler, props.productId]);
|
|
896
|
+
|
|
897
|
+
const addToCart = useCallback(() => {
|
|
898
|
+
if (state.data && props.onAddToCart) {
|
|
899
|
+
props.onAddToCart(state.data.id);
|
|
900
|
+
}
|
|
901
|
+
}, [state.data, props.onAddToCart]);
|
|
902
|
+
|
|
903
|
+
const reset = useCallback(() => {
|
|
904
|
+
handler.reset();
|
|
905
|
+
setState(handler.getState());
|
|
906
|
+
}, [handler]);
|
|
907
|
+
|
|
908
|
+
useEffect(() => {
|
|
909
|
+
if (props.autoLoad) {
|
|
910
|
+
loadProduct();
|
|
911
|
+
}
|
|
912
|
+
}, [props.autoLoad, loadProduct]);
|
|
913
|
+
|
|
914
|
+
return {
|
|
915
|
+
state,
|
|
916
|
+
actions: { loadProduct, addToCart, reset } as IProductCardActions,
|
|
917
|
+
};
|
|
918
|
+
};
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
**Reglas del hook:**
|
|
922
|
+
- ✅ Puente reactivo: entre handler y componente
|
|
923
|
+
- ✅ Estado React: sincronizado con handler
|
|
924
|
+
- ✅ Callbacks memorizados: useCallback para optimizacion
|
|
925
|
+
- ✅ Efectos controlados: auto-load, dependencies
|
|
926
|
+
- ✅ Interface consistente: siempre retorna `{ state, actions }`
|
|
927
|
+
|
|
928
|
+
### Nombres de Archivos
|
|
929
|
+
|
|
930
|
+
```typescript
|
|
931
|
+
// CORRECTO - PascalCase
|
|
932
|
+
ProductCard.tsx
|
|
933
|
+
UserProfile.tsx
|
|
934
|
+
DataTable.tsx
|
|
935
|
+
|
|
936
|
+
// INCORRECTO
|
|
937
|
+
my-component.tsx
|
|
938
|
+
user_profile.tsx
|
|
939
|
+
dataTable.tsx
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
### Generacion con mks-ui CLI
|
|
943
|
+
|
|
944
|
+
```bash
|
|
945
|
+
# Componente UI simple (archivo unico)
|
|
946
|
+
mks-ui component Button --ui
|
|
947
|
+
|
|
948
|
+
# Componente complejo con BLO
|
|
949
|
+
mks-ui component ProductCard --complex
|
|
950
|
+
|
|
951
|
+
# Vista previa sin crear
|
|
952
|
+
mks-ui component TestComponent --dry-run
|
|
953
|
+
|
|
954
|
+
# Servicio backend
|
|
955
|
+
mks-ui service ProductService
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
---
|
|
959
|
+
|
|
960
|
+
## REGLA 9: Zustand con Slices
|
|
961
|
+
|
|
962
|
+
### Store Central por Dominio
|
|
963
|
+
|
|
964
|
+
```typescript
|
|
965
|
+
// stores/app.store.ts
|
|
966
|
+
import { create } from 'zustand';
|
|
967
|
+
|
|
968
|
+
interface IAppStore {
|
|
969
|
+
// UI Slice
|
|
970
|
+
ui: { sidebarOpen: boolean; theme: 'light' | 'dark' };
|
|
971
|
+
toggleSidebar: () => void;
|
|
972
|
+
|
|
973
|
+
// Data Slice
|
|
974
|
+
users: IUser[];
|
|
975
|
+
setUsers: (users: IUser[]) => void;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
export const useAppStore = create<IAppStore>((set) => ({
|
|
979
|
+
ui: { sidebarOpen: true, theme: 'light' },
|
|
980
|
+
toggleSidebar: () => set((s) => ({
|
|
981
|
+
ui: { ...s.ui, sidebarOpen: !s.ui.sidebarOpen }
|
|
982
|
+
})),
|
|
983
|
+
|
|
984
|
+
users: [],
|
|
985
|
+
setUsers: (users) => set({ users }),
|
|
986
|
+
}));
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### Selectores Puros
|
|
990
|
+
|
|
991
|
+
```typescript
|
|
992
|
+
// CORRECTO - Selector especifico previene renders
|
|
993
|
+
const theme = useAppStore((s) => s.ui.theme);
|
|
994
|
+
|
|
995
|
+
// INCORRECTO - Re-render en cualquier cambio
|
|
996
|
+
const store = useAppStore();
|
|
997
|
+
const theme = store.ui.theme;
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
---
|
|
1001
|
+
|
|
1002
|
+
## REGLA 10: Handler Pattern
|
|
1003
|
+
|
|
1004
|
+
### Fachada de Orquestacion
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
// handlers/useUserHandler.ts
|
|
1008
|
+
export const useUserHandler = (service: UserService) => {
|
|
1009
|
+
const { state, load } = useUserLoader(service);
|
|
1010
|
+
const setUsers = useAppStore((s) => s.setUsers);
|
|
1011
|
+
|
|
1012
|
+
const refresh = useCallback(async (id: string) => {
|
|
1013
|
+
const result = await load(id);
|
|
1014
|
+
if (isSuccess(result)) {
|
|
1015
|
+
setUsers([result.data]);
|
|
1016
|
+
}
|
|
1017
|
+
return result;
|
|
1018
|
+
}, [load, setUsers]);
|
|
1019
|
+
|
|
1020
|
+
return useMemo(() => ({
|
|
1021
|
+
isLoading: isPending(state),
|
|
1022
|
+
hasError: isFailed(state),
|
|
1023
|
+
user: hasData(state) ? state.data : null,
|
|
1024
|
+
refresh,
|
|
1025
|
+
}), [state, refresh]);
|
|
1026
|
+
};
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
### Reglas del Handler
|
|
1030
|
+
|
|
1031
|
+
- API publica memorizada con `useMemo`
|
|
1032
|
+
- Callbacks con `useCallback`
|
|
1033
|
+
- No exponer detalles internos (state crudo)
|
|
1034
|
+
- No usar `useEffect` salvo sincronizacion externa
|
|
1035
|
+
|
|
1036
|
+
---
|
|
1037
|
+
|
|
1038
|
+
## REGLA 11: BaseService para HTTP
|
|
1039
|
+
|
|
1040
|
+
### Clase Base
|
|
1041
|
+
|
|
1042
|
+
```typescript
|
|
1043
|
+
export abstract class BaseService {
|
|
1044
|
+
constructor(
|
|
1045
|
+
protected readonly baseUrl: string,
|
|
1046
|
+
protected readonly fetchImpl = fetch
|
|
1047
|
+
) {}
|
|
1048
|
+
|
|
1049
|
+
protected async request<T>({
|
|
1050
|
+
path,
|
|
1051
|
+
method = 'GET',
|
|
1052
|
+
body,
|
|
1053
|
+
timeoutMs = 15000,
|
|
1054
|
+
signal,
|
|
1055
|
+
}: IRequestOptions): Promise<Response> {
|
|
1056
|
+
const controller = new AbortController();
|
|
1057
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1058
|
+
|
|
1059
|
+
try {
|
|
1060
|
+
return await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
1061
|
+
method,
|
|
1062
|
+
headers: { 'content-type': 'application/json' },
|
|
1063
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
1064
|
+
signal: signal ?? controller.signal,
|
|
1065
|
+
});
|
|
1066
|
+
} finally {
|
|
1067
|
+
clearTimeout(timeoutId);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
### Error Taxonomy
|
|
1074
|
+
|
|
1075
|
+
```typescript
|
|
1076
|
+
export type ServiceError =
|
|
1077
|
+
| { kind: 'timeout'; message: string }
|
|
1078
|
+
| { kind: 'unauthorized'; message: string } // 401
|
|
1079
|
+
| { kind: 'forbidden'; message: string } // 403
|
|
1080
|
+
| { kind: 'not_found'; message: string } // 404
|
|
1081
|
+
| { kind: 'conflict'; message: string } // 409
|
|
1082
|
+
| { kind: 'too_many_requests'; message: string } // 429
|
|
1083
|
+
| { kind: 'server_error'; message: string; status: number } // 5xx
|
|
1084
|
+
| { kind: 'decode_error'; message: string }
|
|
1085
|
+
| { kind: 'unexpected'; message: string };
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
### Servicio Tipado
|
|
1089
|
+
|
|
1090
|
+
```typescript
|
|
1091
|
+
export class UserService extends BaseService {
|
|
1092
|
+
async getUser(id: string): Promise<Result<IUser, ServiceError>> {
|
|
1093
|
+
try {
|
|
1094
|
+
const res = await this.request({ path: `/users/${id}` });
|
|
1095
|
+
|
|
1096
|
+
if (res.status === 401) return err({ kind: 'unauthorized', message: 'No autenticado' });
|
|
1097
|
+
if (res.status === 403) return err({ kind: 'forbidden', message: 'No autorizado' });
|
|
1098
|
+
if (res.status === 404) return err({ kind: 'not_found', message: 'Usuario no encontrado' });
|
|
1099
|
+
if (!res.ok) return err({ kind: 'server_error', message: 'Error servidor', status: res.status });
|
|
1100
|
+
|
|
1101
|
+
const json = await res.json();
|
|
1102
|
+
const parsed = UserSchema.safeParse(json);
|
|
1103
|
+
if (!parsed.success) return err({ kind: 'decode_error', message: parsed.error.message });
|
|
1104
|
+
|
|
1105
|
+
return ok(parsed.data);
|
|
1106
|
+
} catch (e) {
|
|
1107
|
+
if ((e as Error)?.name === 'AbortError') {
|
|
1108
|
+
return err({ kind: 'timeout', message: 'Timeout' });
|
|
1109
|
+
}
|
|
1110
|
+
return err({ kind: 'unexpected', message: (e as Error)?.message ?? 'Error inesperado' });
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
---
|
|
1117
|
+
|
|
1118
|
+
## REGLA 12: React Best Practices
|
|
1119
|
+
|
|
1120
|
+
### Minimizar useEffect
|
|
1121
|
+
|
|
1122
|
+
```typescript
|
|
1123
|
+
// CORRECTO - useMemo para derivar datos
|
|
1124
|
+
const filteredUsers = useMemo(
|
|
1125
|
+
() => users.filter(u => u.active),
|
|
1126
|
+
[users]
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
// INCORRECTO - useEffect para derivar datos
|
|
1130
|
+
const [filteredUsers, setFiltered] = useState([]);
|
|
1131
|
+
useEffect(() => {
|
|
1132
|
+
setFiltered(users.filter(u => u.active));
|
|
1133
|
+
}, [users]);
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
### memo para Componentes Puros
|
|
1137
|
+
|
|
1138
|
+
```typescript
|
|
1139
|
+
// CORRECTO
|
|
1140
|
+
export const UserCard = memo(({ user }: IUserCardProps) => (
|
|
1141
|
+
<div>{user.name}</div>
|
|
1142
|
+
));
|
|
1143
|
+
|
|
1144
|
+
// useState solo para estado efimero local
|
|
1145
|
+
const [inputValue, setInputValue] = useState('');
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
---
|
|
1149
|
+
|
|
1150
|
+
## Checklist Pre-Commit
|
|
1151
|
+
|
|
1152
|
+
Antes de hacer commit de codigo, verificar:
|
|
1153
|
+
|
|
1154
|
+
- [ ] Todo codigo nuevo tiene JSDoc completo
|
|
1155
|
+
- [ ] No hay `console.log/debug/error/info/warn`
|
|
1156
|
+
- [ ] Todo lo que puede fallar usa `Result<T, E>`
|
|
1157
|
+
- [ ] Interfaces tienen prefijo `I`
|
|
1158
|
+
- [ ] Barrel exports en todas las carpetas
|
|
1159
|
+
- [ ] Async/await en lugar de Promise chaining
|
|
1160
|
+
- [ ] Componentes UI siguen estructura de archivos
|
|
1161
|
+
- [ ] Estados de UI usan TransactionState
|
|
1162
|
+
- [ ] Stores usan selectores puros
|
|
1163
|
+
- [ ] Handlers exponen API memorizada
|
|
1164
|
+
- [ ] `bun run typecheck` pasa
|
|
1165
|
+
- [ ] `bun run lint` pasa
|
|
1166
|
+
- [ ] `bun run format` aplicado
|
|
1167
|
+
|
|
1168
|
+
---
|
|
1169
|
+
|
|
1170
|
+
## REGLA 13: UI Components - Shadcn/UI + Animate UI
|
|
1171
|
+
|
|
1172
|
+
### Stack de Componentes UI
|
|
1173
|
+
|
|
1174
|
+
**OBLIGATORIO**: Shadcn/UI como base, con **Animate UI como fuente principal** de componentes animados.
|
|
1175
|
+
|
|
1176
|
+
### Formato de Comandos
|
|
1177
|
+
|
|
1178
|
+
```bash
|
|
1179
|
+
# CORRECTO - Formato estandar con Bun
|
|
1180
|
+
bunx --bun shadcn@latest add @animate-ui/primitives-texts-sliding-number
|
|
1181
|
+
bunx --bun shadcn@latest add @animate-ui/primitives-button
|
|
1182
|
+
bunx --bun shadcn@latest add @animate-ui/primitives-card
|
|
1183
|
+
|
|
1184
|
+
# CORRECTO - Componentes base Shadcn (cuando no exista en Animate UI)
|
|
1185
|
+
bunx --bun shadcn@latest add button
|
|
1186
|
+
bunx --bun shadcn@latest add card
|
|
1187
|
+
bunx --bun shadcn@latest add dialog
|
|
1188
|
+
|
|
1189
|
+
# Ver registry disponible
|
|
1190
|
+
# https://ui.shadcn.com/docs/mcp
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
### Fuentes de Componentes
|
|
1194
|
+
|
|
1195
|
+
| Fuente | Prefijo | Tipo | Prioridad |
|
|
1196
|
+
|--------|---------|------|-----------|
|
|
1197
|
+
| **Animate UI** | `@animate-ui/primitives-*` | Componentes con animaciones | **PREFERIDO** |
|
|
1198
|
+
| **Shadcn Registry** | `shadcn/*` | Componentes base estándar | Alternativa |
|
|
1199
|
+
| **Radix UI** | `@radix-ui/*` | Primitivas sin estilos | Solo si es necesario |
|
|
1200
|
+
|
|
1201
|
+
### Configuracion de components.json
|
|
1202
|
+
|
|
1203
|
+
```json
|
|
1204
|
+
{
|
|
1205
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
1206
|
+
"style": "new-york",
|
|
1207
|
+
"rsc": false,
|
|
1208
|
+
"tsx": true,
|
|
1209
|
+
"tailwind": {
|
|
1210
|
+
"config": "tailwind.config.ts",
|
|
1211
|
+
"css": "src/app/globals.css",
|
|
1212
|
+
"baseColor": "neutral",
|
|
1213
|
+
"cssVariables": true,
|
|
1214
|
+
"prefix": ""
|
|
1215
|
+
},
|
|
1216
|
+
"aliases": {
|
|
1217
|
+
"components": "@/components",
|
|
1218
|
+
"utils": "@/lib/utils",
|
|
1219
|
+
"ui": "@/components/ui"
|
|
1220
|
+
},
|
|
1221
|
+
"registry": "https://ui.shadcn.com"
|
|
1222
|
+
}
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
### Patrones de Importacion
|
|
1226
|
+
|
|
1227
|
+
```typescript
|
|
1228
|
+
// CORRECTO - Importar desde components/ui
|
|
1229
|
+
import { Button } from '@/components/ui/button';
|
|
1230
|
+
import { Card } from '@/components/ui/card';
|
|
1231
|
+
|
|
1232
|
+
// CORRECTO - Componentes Animate UI
|
|
1233
|
+
import { SlidingNumber } from '@/components/ui/sliding-number';
|
|
1234
|
+
import { AnimatedCard } from '@/components/ui/animated-card';
|
|
1235
|
+
import { MorphingText } from '@/components/ui/morphing-text';
|
|
1236
|
+
|
|
1237
|
+
// INCORRECTO - Importar directo desde node_modules
|
|
1238
|
+
import { Button } from '@radix-ui/react-button';
|
|
1239
|
+
```
|
|
1240
|
+
|
|
1241
|
+
### Componentes Recomendados (Animate UI)
|
|
1242
|
+
|
|
1243
|
+
| Componente | Prefijo | Uso |
|
|
1244
|
+
|-----------|---------|-----|
|
|
1245
|
+
| `sliding-number` | `@animate-ui/primitives-texts-*` | Números animados, contadores |
|
|
1246
|
+
| `morphing-text` | `@animate-ui/primitives-texts-*` | Texto que transforma entre valores |
|
|
1247
|
+
| `animated-card` | `@animate-ui/primitives-card*` | Tarjetas con animaciones de entrada |
|
|
1248
|
+
| `progress-reveal` | `@animate-ui/primitives-progress-*` | Progress con animación suave |
|
|
1249
|
+
| `slide-toggle` | `@animate-ui/primitives-toggle-*` | Switch con animación |
|
|
1250
|
+
|
|
1251
|
+
### Componentes Base Shadcn (cuando Animate UI no disponible)
|
|
1252
|
+
|
|
1253
|
+
```bash
|
|
1254
|
+
# Instalar componentes base esenciales
|
|
1255
|
+
bunx --bun shadcn@latest add button
|
|
1256
|
+
bunx --bun shadcn@latest add card
|
|
1257
|
+
bunx --bun shadcn@latest add input
|
|
1258
|
+
bunx --bun shadcn@latest add textarea
|
|
1259
|
+
bunx --bun shadcn@latest add accordion
|
|
1260
|
+
bunx --bun shadcn@latest add dialog
|
|
1261
|
+
bunx --bun shadcn@latest add dropdown-menu
|
|
1262
|
+
bunx --bun shadcn@latest add toast
|
|
1263
|
+
bunx --bun shadcn@latest add tooltip
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
### Inicializar Shadcn/UI (Primera vez)
|
|
1267
|
+
|
|
1268
|
+
```bash
|
|
1269
|
+
# En el directorio del app (ej: apps/devenv-agent-ui)
|
|
1270
|
+
bunx --bun shadcn@latest init
|
|
1271
|
+
|
|
1272
|
+
# Responder las preguntas:
|
|
1273
|
+
# - Style: new-york (recomendado)
|
|
1274
|
+
# - Base color: neutral
|
|
1275
|
+
# - CSS variables: true
|
|
1276
|
+
# - Tailwind config: tailwind.config.ts
|
|
1277
|
+
# - Components path: @/components
|
|
1278
|
+
# - Utils path: @/lib/utils
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
### Ejemplo Completo de Uso
|
|
1282
|
+
|
|
1283
|
+
```typescript
|
|
1284
|
+
// components/agent/MessageItem.tsx
|
|
1285
|
+
import { AnimatedCard } from '@/components/ui/animated-card';
|
|
1286
|
+
import { MorphingText } from '@/components/ui/morphing-text';
|
|
1287
|
+
import { SlidingNumber } from '@/components/ui/sliding-number';
|
|
1288
|
+
import type { IMessage } from './MessageItem.types';
|
|
1289
|
+
|
|
1290
|
+
export function MessageItem({ message }: IMessageItemProps) {
|
|
1291
|
+
return (
|
|
1292
|
+
<AnimatedCard className="p-4">
|
|
1293
|
+
<div className="flex items-center gap-2">
|
|
1294
|
+
<MorphingText>{message.content}</MorphingText>
|
|
1295
|
+
{message.progress && (
|
|
1296
|
+
<SlidingNumber value={message.progress.percentage} />
|
|
1297
|
+
)}
|
|
1298
|
+
</div>
|
|
1299
|
+
</AnimatedCard>
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
### MCP para Buscar Componentes
|
|
1305
|
+
|
|
1306
|
+
```bash
|
|
1307
|
+
# Usar el MCP server de Shadcn para buscar componentes
|
|
1308
|
+
# Ver documentacion: https://ui.shadcn.com/docs/mcp
|
|
1309
|
+
|
|
1310
|
+
# Ejemplos de busqueda via MCP:
|
|
1311
|
+
# - "text animation components"
|
|
1312
|
+
# - "card with entrance animation"
|
|
1313
|
+
# - "progress with reveal effect"
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
---
|
|
1317
|
+
|
|
1318
|
+
## REGLA 14: Iconos Animados - lucie-animated
|
|
1319
|
+
|
|
1320
|
+
### Stack de Iconos
|
|
1321
|
+
|
|
1322
|
+
**OBLIGATORIO**: lucie-animated como fuente principal para iconos animados.
|
|
1323
|
+
|
|
1324
|
+
### Formato de Comandos
|
|
1325
|
+
|
|
1326
|
+
```bash
|
|
1327
|
+
# CORRECTO - Formato estandar con Bun
|
|
1328
|
+
bunx --bun shadcn@latest add @lucie-animated/a-arrow-up
|
|
1329
|
+
bunx --bun shadcn@latest add @lucie-animated/arrow-down
|
|
1330
|
+
bunx --bun shadcn@latest add @lucie-animated/check-circle
|
|
1331
|
+
bunx --bun shadcn@latest add @lucie-animated/x-circle
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
### Fuentes de Iconos
|
|
1335
|
+
|
|
1336
|
+
| Fuente | Prefijo | Tipo | Prioridad |
|
|
1337
|
+
|--------|---------|------|-----------|
|
|
1338
|
+
| **lucie-animated** | `@lucie-animated/*` | Iconos animados | **PREFERIDO** |
|
|
1339
|
+
| **lucide-react** | `lucide-react` | Iconos estándar sin animar | Alternativa |
|
|
1340
|
+
| **Radix Icons** | `@radix-ui/icons` | Iconos Radix | Solo si es necesario |
|
|
1341
|
+
|
|
1342
|
+
### Catalogo Disponible
|
|
1343
|
+
|
|
1344
|
+
```bash
|
|
1345
|
+
# Ver catalogo completo en:
|
|
1346
|
+
# https://lucie-animated.com/
|
|
1347
|
+
|
|
1348
|
+
# Iconos comunes disponibles:
|
|
1349
|
+
# - Flechas: a-arrow-up, a-arrow-down, a-arrow-left, a-arrow-right
|
|
1350
|
+
# - Acciones: check-circle, x-circle, plus-circle, minus-circle
|
|
1351
|
+
# - Navegacion: home, settings, user, bell, search
|
|
1352
|
+
# - Archivos: file, folder, download, upload
|
|
1353
|
+
# - Media: play, pause, volume, volume-x
|
|
1354
|
+
```
|
|
1355
|
+
|
|
1356
|
+
### Patrones de Importacion
|
|
1357
|
+
|
|
1358
|
+
```typescript
|
|
1359
|
+
// CORRECTO - Importar desde components/icons
|
|
1360
|
+
import { AArrowUp } from '@/components/icons/a-arrow-up';
|
|
1361
|
+
import { ArrowDown } from '@/components/icons/arrow-down';
|
|
1362
|
+
import { CheckCircle } from '@/components/icons/check-circle';
|
|
1363
|
+
import { XCircle } from '@/components/icons/x-circle';
|
|
1364
|
+
|
|
1365
|
+
// CORRECTO - Icono estatico (cuando no existe version animada)
|
|
1366
|
+
import { Settings } from 'lucide-react';
|
|
1367
|
+
|
|
1368
|
+
// INCORRECTO - Importar directo desde libreria
|
|
1369
|
+
import { AArrowUp } from '@lucie-animated/a-arrow-up';
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
### Estructura de Carpeta
|
|
1373
|
+
|
|
1374
|
+
```
|
|
1375
|
+
components/
|
|
1376
|
+
└── icons/
|
|
1377
|
+
├── a-arrow-up.tsx # Icono animado
|
|
1378
|
+
├── arrow-down.tsx # Icono animado
|
|
1379
|
+
├── check-circle.tsx # Icono animado
|
|
1380
|
+
└── index.ts # Barrel export
|
|
1381
|
+
```
|
|
1382
|
+
|
|
1383
|
+
### Ejemplo Completo de Uso
|
|
1384
|
+
|
|
1385
|
+
```typescript
|
|
1386
|
+
// components/agent/AgentControls.tsx
|
|
1387
|
+
import { AArrowUp } from '@/components/icons/a-arrow-up';
|
|
1388
|
+
import { Play } from '@/components/icons/play';
|
|
1389
|
+
import { CheckCircle } from '@/components/icons/check-circle';
|
|
1390
|
+
import type { IAgentControlsProps } from './AgentControls.types';
|
|
1391
|
+
|
|
1392
|
+
export function AgentControls({ isRunning, onExecute }: IAgentControlsProps) {
|
|
1393
|
+
return (
|
|
1394
|
+
<div className="flex gap-2">
|
|
1395
|
+
<button onClick={onExecute}>
|
|
1396
|
+
{isRunning ? (
|
|
1397
|
+
<CheckCircle className="w-5 h-5" />
|
|
1398
|
+
) : (
|
|
1399
|
+
<Play className="w-5 h-5" />
|
|
1400
|
+
)}
|
|
1401
|
+
</button>
|
|
1402
|
+
<AArrowUp className="w-5 h-5 animate-bounce" />
|
|
1403
|
+
</div>
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
### Referencias
|
|
1409
|
+
|
|
1410
|
+
- **lucie-animated Gallery**: https://lucie-animated.com/
|
|
1411
|
+
- **lucide-react Documentation**: https://lucide.dev/
|
|
1412
|
+
- **Shadcn Icons**: https://ui.shadcn.com/docs/components/icon
|
|
1413
|
+
|
|
1414
|
+
### Referencias
|
|
1415
|
+
|
|
1416
|
+
- **Shadcn/UI Documentation**: https://ui.shadcn.com/
|
|
1417
|
+
- **Shadcn MCP Registry**: https://ui.shadcn.com/docs/mcp
|
|
1418
|
+
- **Animate UI Gallery**: https://animate.ui/
|
|
1419
|
+
- **Radix UI Primitives**: https://www.radix-ui.com/
|
|
1420
|
+
- **CLI Documentation**: https://ui.shadcn.com/docs/cli
|
|
1421
|
+
|
|
1422
|
+
---
|
|
1423
|
+
|
|
1424
|
+
## REGLA 15: Testing - Vitest (NO bun test)
|
|
1425
|
+
|
|
1426
|
+
### Runner Correcto
|
|
1427
|
+
|
|
1428
|
+
**OBLIGATORIO**: Usar `vitest` o `bunx vitest` para ejecutar tests. NUNCA `bun test`.
|
|
1429
|
+
|
|
1430
|
+
```bash
|
|
1431
|
+
# CORRECTO - Vitest runner
|
|
1432
|
+
vitest # Modo watch
|
|
1433
|
+
vitest run # Single run
|
|
1434
|
+
bunx vitest run # Con bunx
|
|
1435
|
+
bunx vitest run src/hooks/ # Tests específicos
|
|
1436
|
+
|
|
1437
|
+
# INCORRECTO - Bun test runner
|
|
1438
|
+
bun test # NO USAR
|
|
1439
|
+
bun test src/ # NO USAR
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
### Configuración Base
|
|
1443
|
+
|
|
1444
|
+
```typescript
|
|
1445
|
+
// vitest.config.ts
|
|
1446
|
+
import { defineConfig } from 'vitest/config';
|
|
1447
|
+
import react from '@vitejs/plugin-react';
|
|
1448
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
1449
|
+
|
|
1450
|
+
export default defineConfig({
|
|
1451
|
+
plugins: [react(), tsconfigPaths()],
|
|
1452
|
+
test: {
|
|
1453
|
+
environment: 'jsdom',
|
|
1454
|
+
setupFiles: ['./src/tests/setupTests.ts'],
|
|
1455
|
+
globals: true,
|
|
1456
|
+
},
|
|
1457
|
+
});
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
### Setup con MSW
|
|
1461
|
+
|
|
1462
|
+
```typescript
|
|
1463
|
+
// src/tests/setupTests.ts
|
|
1464
|
+
import '@testing-library/jest-dom';
|
|
1465
|
+
import { beforeAll, afterEach, afterAll } from 'vitest';
|
|
1466
|
+
import { server } from './mocks/server';
|
|
1467
|
+
|
|
1468
|
+
beforeAll(() => server.listen());
|
|
1469
|
+
afterEach(() => {
|
|
1470
|
+
server.resetHandlers();
|
|
1471
|
+
localStorage.clear();
|
|
1472
|
+
});
|
|
1473
|
+
afterAll(() => server.close());
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
### Documentación Completa
|
|
1477
|
+
|
|
1478
|
+
Ver documentación detallada en:
|
|
1479
|
+
- `~/dotfiles/mks-ui/docs/testing-architecture.md`
|
|
1480
|
+
- `~/dotfiles/mks-ui/docs/testing-strategy.md`
|
|
1481
|
+
|
|
1482
|
+
---
|
|
1483
|
+
|
|
1484
|
+
## REGLA 16: Sistema de Themes - CSS Variables + Theme Manager
|
|
1485
|
+
|
|
1486
|
+
### Stack de Theming
|
|
1487
|
+
|
|
1488
|
+
**OBLIGATORIO**: Usar sistema de CSS variables con tokens apropiados.
|
|
1489
|
+
|
|
1490
|
+
| Package | Uso | Instalación |
|
|
1491
|
+
|---------|-----|-------------|
|
|
1492
|
+
| **@mks2508/theme-manager-react** | Theme switching con animaciones | `bun add @mks2508/theme-manager-react` |
|
|
1493
|
+
| **@mks2508/shadcn-basecoat-theme-manager** | Core de gestión de temas | `bun add @mks2508/shadcn-basecoat-theme-manager` |
|
|
1494
|
+
|
|
1495
|
+
### Variables CSS Requeridas
|
|
1496
|
+
|
|
1497
|
+
```css
|
|
1498
|
+
:root {
|
|
1499
|
+
/* Colores base */
|
|
1500
|
+
--background: 0 0% 100%;
|
|
1501
|
+
--foreground: 222.2 84% 4.9%;
|
|
1502
|
+
--primary: 222.2 47.4% 11.2%;
|
|
1503
|
+
--primary-foreground: 210 40% 98%;
|
|
1504
|
+
--secondary: 210 40% 96%;
|
|
1505
|
+
--secondary-foreground: 222.2 84% 4.9%;
|
|
1506
|
+
--muted: 210 40% 96%;
|
|
1507
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
1508
|
+
--accent: 210 40% 96%;
|
|
1509
|
+
--accent-foreground: 222.2 84% 4.9%;
|
|
1510
|
+
--destructive: 0 84.2% 60.2%;
|
|
1511
|
+
--destructive-foreground: 210 40% 98%;
|
|
1512
|
+
|
|
1513
|
+
/* UI Elements */
|
|
1514
|
+
--card: var(--background);
|
|
1515
|
+
--card-foreground: var(--foreground);
|
|
1516
|
+
--popover: var(--background);
|
|
1517
|
+
--popover-foreground: var(--foreground);
|
|
1518
|
+
--border: 214.3 31.8% 91.4%;
|
|
1519
|
+
--input: 214.3 31.8% 91.4%;
|
|
1520
|
+
--ring: 222.2 84% 4.9%;
|
|
1521
|
+
--radius: 0.5rem;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
.dark {
|
|
1525
|
+
--background: 222.2 84% 4.9%;
|
|
1526
|
+
--foreground: 210 40% 98%;
|
|
1527
|
+
/* ... dark variants */
|
|
1528
|
+
}
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
### Uso con Next.js
|
|
1532
|
+
|
|
1533
|
+
```tsx
|
|
1534
|
+
// app/layout.tsx
|
|
1535
|
+
import { ThemeProvider } from '@mks2508/theme-manager-react/nextjs'
|
|
1536
|
+
|
|
1537
|
+
export default function RootLayout({ children }) {
|
|
1538
|
+
return (
|
|
1539
|
+
<html lang="en">
|
|
1540
|
+
<body>
|
|
1541
|
+
<ThemeProvider
|
|
1542
|
+
registryUrl="/themes/registry.json"
|
|
1543
|
+
defaultTheme="default"
|
|
1544
|
+
defaultMode="auto"
|
|
1545
|
+
enableTransitions={true}
|
|
1546
|
+
>
|
|
1547
|
+
{children}
|
|
1548
|
+
</ThemeProvider>
|
|
1549
|
+
</body>
|
|
1550
|
+
</html>
|
|
1551
|
+
)
|
|
1552
|
+
}
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
### Componentes de Theme
|
|
1556
|
+
|
|
1557
|
+
```tsx
|
|
1558
|
+
import {
|
|
1559
|
+
ModeToggle, // Toggle light/dark
|
|
1560
|
+
AnimatedThemeToggler, // Toggle con animaciones
|
|
1561
|
+
ThemeSelector // Selector completo de temas
|
|
1562
|
+
} from '@mks2508/theme-manager-react/nextjs'
|
|
1563
|
+
```
|
|
1564
|
+
|
|
1565
|
+
---
|
|
1566
|
+
|
|
1567
|
+
## REGLA 17: Sidebar y Bottom Navigation - sidebar-headless
|
|
1568
|
+
|
|
1569
|
+
### Package Oficial
|
|
1570
|
+
|
|
1571
|
+
**OBLIGATORIO**: Usar `@mks2508/sidebar-headless` para sidebars y bottom navigation.
|
|
1572
|
+
|
|
1573
|
+
```bash
|
|
1574
|
+
bun add @mks2508/sidebar-headless
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
### Características
|
|
1578
|
+
|
|
1579
|
+
- Headless sidebar y mobile bottom navigation
|
|
1580
|
+
- Animaciones fluidas
|
|
1581
|
+
- Keyboard navigation completa
|
|
1582
|
+
- WAI-ARIA accessibility
|
|
1583
|
+
- Soporte glassmorphism
|
|
1584
|
+
- Mobile-first design
|
|
1585
|
+
|
|
1586
|
+
### Ejemplo de Uso
|
|
1587
|
+
|
|
1588
|
+
```tsx
|
|
1589
|
+
import {
|
|
1590
|
+
Sidebar,
|
|
1591
|
+
SidebarItem,
|
|
1592
|
+
BottomNavigation
|
|
1593
|
+
} from '@mks2508/sidebar-headless';
|
|
1594
|
+
|
|
1595
|
+
export function AppLayout({ children }) {
|
|
1596
|
+
return (
|
|
1597
|
+
<div className="flex">
|
|
1598
|
+
{/* Desktop Sidebar */}
|
|
1599
|
+
<Sidebar className="hidden md:flex">
|
|
1600
|
+
<SidebarItem icon={<Home />} label="Home" href="/" />
|
|
1601
|
+
<SidebarItem icon={<Settings />} label="Settings" href="/settings" />
|
|
1602
|
+
</Sidebar>
|
|
1603
|
+
|
|
1604
|
+
{/* Mobile Bottom Nav */}
|
|
1605
|
+
<BottomNavigation className="md:hidden">
|
|
1606
|
+
<SidebarItem icon={<Home />} label="Home" href="/" />
|
|
1607
|
+
<SidebarItem icon={<Settings />} label="Settings" href="/settings" />
|
|
1608
|
+
</BottomNavigation>
|
|
1609
|
+
|
|
1610
|
+
<main>{children}</main>
|
|
1611
|
+
</div>
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
---
|
|
1617
|
+
|
|
1618
|
+
## REGLA 18: Glassmorphism - Guía de Implementación
|
|
1619
|
+
|
|
1620
|
+
### Principios Fundamentales
|
|
1621
|
+
|
|
1622
|
+
Glassmorphism es la combinación controlada de:
|
|
1623
|
+
- **Transparencia parcial** (RGBA / alpha 10-30%)
|
|
1624
|
+
- **Backdrop blur** (difuminar lo que hay detrás)
|
|
1625
|
+
- **Bordes sutiles** (simular refracción)
|
|
1626
|
+
- **Sombras suaves** (profundidad)
|
|
1627
|
+
- **isolation: isolate** (stacking context)
|
|
1628
|
+
|
|
1629
|
+
### Receta Base (Tailwind)
|
|
1630
|
+
|
|
1631
|
+
```html
|
|
1632
|
+
<div class="
|
|
1633
|
+
isolate
|
|
1634
|
+
rounded-xl
|
|
1635
|
+
bg-white/20
|
|
1636
|
+
backdrop-blur-lg
|
|
1637
|
+
border border-white/30
|
|
1638
|
+
shadow-xl shadow-black/10
|
|
1639
|
+
">
|
|
1640
|
+
</div>
|
|
1641
|
+
```
|
|
1642
|
+
|
|
1643
|
+
### Clases Glassmorphism Disponibles
|
|
1644
|
+
|
|
1645
|
+
```css
|
|
1646
|
+
/* Paneles principales */
|
|
1647
|
+
.glass-panel /* blur-16px, borde primary */
|
|
1648
|
+
.glass-panel-subtle /* blur-8px, más transparente */
|
|
1649
|
+
.glass-panel-heavy /* blur-24px, más opaco */
|
|
1650
|
+
|
|
1651
|
+
/* Cards de producto */
|
|
1652
|
+
.product-card-glass /* blur-12px con hover */
|
|
1653
|
+
.product-card-glass-enhanced /* blur-16px con glow */
|
|
1654
|
+
|
|
1655
|
+
/* Badges */
|
|
1656
|
+
.badge-glass-featured /* Primary con glow */
|
|
1657
|
+
.badge-glass-subtle /* Sutil, borde transparente */
|
|
1658
|
+
.badge-glass-outline /* Solo borde */
|
|
1659
|
+
|
|
1660
|
+
/* Inputs y formularios */
|
|
1661
|
+
.input-glass /* Blur sutil con focus glow */
|
|
1662
|
+
|
|
1663
|
+
/* Headers */
|
|
1664
|
+
.bg-glass-header /* Desktop header */
|
|
1665
|
+
.bg-glass-header-mobile /* Mobile header más transparente */
|
|
1666
|
+
```
|
|
1667
|
+
|
|
1668
|
+
### Reglas de Uso
|
|
1669
|
+
|
|
1670
|
+
| Usar en | Evitar en |
|
|
1671
|
+
|---------|-----------|
|
|
1672
|
+
| Headers flotantes | Tablas densas |
|
|
1673
|
+
| Modales | Formularios largos |
|
|
1674
|
+
| Sidebars | Texto extenso |
|
|
1675
|
+
| Cards destacadas | Listas largas |
|
|
1676
|
+
|
|
1677
|
+
### Performance
|
|
1678
|
+
|
|
1679
|
+
```css
|
|
1680
|
+
/* Fallback obligatorio */
|
|
1681
|
+
@supports not (backdrop-filter: blur(1px)) {
|
|
1682
|
+
.glass-panel { background: rgba(255,255,255,0.9); }
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/* Reduced motion */
|
|
1686
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1687
|
+
.glass-panel { transition: none; }
|
|
1688
|
+
}
|
|
1689
|
+
```
|
|
1690
|
+
|
|
1691
|
+
### Documentación Completa
|
|
1692
|
+
|
|
1693
|
+
- `~/dotfiles/mks-ui/docs/glassmorphism-guide.md` - Guía teórica
|
|
1694
|
+
- `~/dotfiles/mks-ui/docs/glassmorphism.css` - Implementación CSS completa
|
|
1695
|
+
|
|
1696
|
+
---
|
|
1697
|
+
|
|
262
1698
|
## Fuentes de Referencia
|
|
263
1699
|
|
|
264
1700
|
- **CLAUDE.md** - Guia de arquitectura del monorepo
|
|
265
1701
|
- **@mks2508/better-logger** - Documentacion del logger
|
|
266
1702
|
- **@mks2508/no-throw** - Documentacion del Result pattern
|
|
1703
|
+
- **@mks2508/theme-manager-react** - Sistema de themes
|
|
1704
|
+
- **@mks2508/sidebar-headless** - Sidebar y bottom navigation
|
|
267
1705
|
- **Arktype** - https://arktype.io/
|
|
268
1706
|
- **Rolldown** - https://rollup.rs/
|
|
269
1707
|
- **Oxlint** - https://oxlint.com/
|
|
1708
|
+
- **Zustand** - https://zustand-demo.pmnd.rs/
|
|
1709
|
+
- **Shadcn/UI** - https://ui.shadcn.com/
|
|
1710
|
+
- **Animate UI** - https://animate.ui/
|
|
1711
|
+
- **lucie-animated** - https://lucie-animated.com/
|
|
1712
|
+
|
|
1713
|
+
### Documentación en Dotfiles
|
|
1714
|
+
|
|
1715
|
+
| Documento | Ubicación | Contenido |
|
|
1716
|
+
|-----------|-----------|-----------|
|
|
1717
|
+
| **Glassmorphism Guide** | `~/dotfiles/mks-ui/docs/glassmorphism-guide.md` | Teoría y best practices |
|
|
1718
|
+
| **Glassmorphism CSS** | `~/dotfiles/mks-ui/docs/glassmorphism.css` | Implementación completa |
|
|
1719
|
+
| **Testing Architecture** | `~/dotfiles/mks-ui/docs/testing-architecture.md` | Arquitectura de tests |
|
|
1720
|
+
| **Testing Strategy** | `~/dotfiles/mks-ui/docs/testing-strategy.md` | Estrategia de testing |
|
|
1721
|
+
|
|
1722
|
+
### CLI mks-ui
|
|
1723
|
+
|
|
1724
|
+
Generador de componentes disponible globalmente:
|
|
1725
|
+
|
|
1726
|
+
```bash
|
|
1727
|
+
mks-ui component Button --ui # Componente UI simple
|
|
1728
|
+
mks-ui component ProductCard --complex # BLO completo
|
|
1729
|
+
mks-ui service ProductService # Servicio Elysia
|
|
1730
|
+
mks-ui hook useProducts # Custom hook
|
|
1731
|
+
mks-ui type Product # Tipos TypeScript
|
|
1732
|
+
```
|