elsabro 2.0.1 → 2.2.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.
Files changed (46) hide show
  1. package/commands/elsabro/add-phase.md +17 -0
  2. package/commands/elsabro/add-todo.md +111 -53
  3. package/commands/elsabro/audit-milestone.md +19 -0
  4. package/commands/elsabro/check-todos.md +210 -31
  5. package/commands/elsabro/complete-milestone.md +20 -1
  6. package/commands/elsabro/debug.md +19 -0
  7. package/commands/elsabro/discuss-phase.md +18 -1
  8. package/commands/elsabro/execute.md +496 -52
  9. package/commands/elsabro/insert-phase.md +18 -1
  10. package/commands/elsabro/list-phase-assumptions.md +17 -0
  11. package/commands/elsabro/new-milestone.md +19 -0
  12. package/commands/elsabro/new.md +19 -0
  13. package/commands/elsabro/pause-work.md +75 -0
  14. package/commands/elsabro/plan-milestone-gaps.md +20 -1
  15. package/commands/elsabro/plan.md +264 -36
  16. package/commands/elsabro/progress.md +203 -79
  17. package/commands/elsabro/quick.md +19 -0
  18. package/commands/elsabro/remove-phase.md +17 -0
  19. package/commands/elsabro/research-phase.md +18 -1
  20. package/commands/elsabro/resume-work.md +130 -2
  21. package/commands/elsabro/start.md +365 -98
  22. package/commands/elsabro/verify-work.md +271 -12
  23. package/package.json +1 -1
  24. package/references/SYSTEM_INDEX.md +241 -0
  25. package/references/command-flow.md +352 -0
  26. package/references/enforcement-rules.md +331 -0
  27. package/references/error-handling-instructions.md +26 -12
  28. package/references/state-sync.md +381 -0
  29. package/references/task-dispatcher.md +388 -0
  30. package/references/tasks-integration.md +380 -0
  31. package/skills/api-microservice.md +765 -0
  32. package/skills/api-setup.md +76 -3
  33. package/skills/auth-setup.md +46 -6
  34. package/skills/chrome-extension.md +584 -0
  35. package/skills/cicd-setup.md +1206 -0
  36. package/skills/cli-tool.md +884 -0
  37. package/skills/database-setup.md +41 -5
  38. package/skills/desktop-app.md +1351 -0
  39. package/skills/expo-app.md +35 -2
  40. package/skills/full-stack-app.md +543 -0
  41. package/skills/mobile-app.md +813 -0
  42. package/skills/nextjs-app.md +33 -2
  43. package/skills/payments-setup.md +76 -1
  44. package/skills/saas-starter.md +639 -0
  45. package/skills/sentry-setup.md +41 -7
  46. package/skills/testing-setup.md +1218 -0
@@ -0,0 +1,813 @@
1
+ ---
2
+ name: mobile-app
3
+ description: Crear app movil con Expo SDK 52+, React Navigation, Expo Router, NativeWind para styling, push notifications y deep linking.
4
+ tags: [expo, react-native, mobile, ios, android, nativewind, push-notifications]
5
+ difficulty: intermediate
6
+ estimated_time: 45min
7
+ ---
8
+
9
+ # Skill: Mobile App
10
+
11
+ <when_to_use>
12
+ Usar cuando el usuario menciona:
13
+ - "app movil"
14
+ - "app para iOS/Android"
15
+ - "React Native"
16
+ - "Expo"
17
+ - "aplicacion nativa"
18
+ </when_to_use>
19
+
20
+ <pre_requisites>
21
+ ## Pre-requisitos
22
+
23
+ - Node.js 20+
24
+ - Expo CLI: `npm install -g expo-cli`
25
+ - Expo Go app en dispositivo (para testing)
26
+ - Para builds nativos: Xcode (iOS) y/o Android Studio
27
+ </pre_requisites>
28
+
29
+ <project_structure>
30
+ ## Estructura de Proyecto
31
+
32
+ ```
33
+ my-mobile-app/
34
+ ├── app/
35
+ │ ├── (tabs)/
36
+ │ │ ├── _layout.tsx # Tab navigator
37
+ │ │ ├── index.tsx # Home tab
38
+ │ │ ├── explore.tsx # Explore tab
39
+ │ │ └── profile.tsx # Profile tab
40
+ │ ├── (auth)/
41
+ │ │ ├── login.tsx
42
+ │ │ └── register.tsx
43
+ │ ├── [id].tsx # Dynamic route
44
+ │ ├── _layout.tsx # Root layout
45
+ │ └── +not-found.tsx # 404 page
46
+ ├── components/
47
+ │ ├── ui/
48
+ │ │ ├── Button.tsx
49
+ │ │ ├── Input.tsx
50
+ │ │ └── Card.tsx
51
+ │ └── ThemedView.tsx
52
+ ├── hooks/
53
+ │ ├── useAuth.ts
54
+ │ └── useNotifications.ts
55
+ ├── stores/
56
+ │ └── authStore.ts # Zustand store
57
+ ├── services/
58
+ │ └── api.ts # API client
59
+ ├── constants/
60
+ │ └── Colors.ts
61
+ ├── assets/
62
+ │ ├── fonts/
63
+ │ └── images/
64
+ ├── app.json # Expo config
65
+ ├── tailwind.config.js # NativeWind config
66
+ ├── babel.config.js
67
+ ├── metro.config.js
68
+ └── package.json
69
+ ```
70
+ </project_structure>
71
+
72
+ <setup_steps>
73
+ ## Pasos de Setup
74
+
75
+ ### Paso 1: Crear proyecto Expo
76
+
77
+ ```bash
78
+ npx create-expo-app@latest my-mobile-app --template tabs
79
+ cd my-mobile-app
80
+ ```
81
+
82
+ ### Paso 2: Instalar dependencias
83
+
84
+ ```bash
85
+ # NativeWind (Tailwind para React Native)
86
+ npm install nativewind tailwindcss
87
+ npx tailwindcss init
88
+
89
+ # Estado global
90
+ npm install zustand
91
+
92
+ # Navegacion adicional
93
+ npm install @react-navigation/native @react-navigation/stack
94
+
95
+ # Storage persistente
96
+ npm install @react-native-async-storage/async-storage
97
+
98
+ # Push notifications
99
+ npx expo install expo-notifications expo-device expo-constants
100
+
101
+ # Linking
102
+ npx expo install expo-linking
103
+ ```
104
+
105
+ ### Paso 3: Configurar NativeWind
106
+
107
+ Editar `tailwind.config.js`:
108
+
109
+ ```javascript
110
+ /** @type {import('tailwindcss').Config} */
111
+ module.exports = {
112
+ content: [
113
+ "./app/**/*.{js,jsx,ts,tsx}",
114
+ "./components/**/*.{js,jsx,ts,tsx}",
115
+ ],
116
+ presets: [require("nativewind/preset")],
117
+ theme: {
118
+ extend: {
119
+ colors: {
120
+ primary: "#007AFF",
121
+ secondary: "#5856D6",
122
+ },
123
+ },
124
+ },
125
+ plugins: [],
126
+ };
127
+ ```
128
+
129
+ Editar `babel.config.js`:
130
+
131
+ ```javascript
132
+ module.exports = function (api) {
133
+ api.cache(true);
134
+ return {
135
+ presets: [
136
+ ["babel-preset-expo", { jsxImportSource: "nativewind" }],
137
+ "nativewind/babel",
138
+ ],
139
+ };
140
+ };
141
+ ```
142
+
143
+ Crear `global.css`:
144
+
145
+ ```css
146
+ @tailwind base;
147
+ @tailwind components;
148
+ @tailwind utilities;
149
+ ```
150
+
151
+ Actualizar `app/_layout.tsx`:
152
+
153
+ ```typescript
154
+ import "../global.css";
155
+ import { Stack } from "expo-router";
156
+ import { useColorScheme } from "react-native";
157
+
158
+ export default function RootLayout() {
159
+ const colorScheme = useColorScheme();
160
+
161
+ return (
162
+ <Stack
163
+ screenOptions={{
164
+ headerStyle: {
165
+ backgroundColor: colorScheme === "dark" ? "#000" : "#fff",
166
+ },
167
+ headerTintColor: colorScheme === "dark" ? "#fff" : "#000",
168
+ }}
169
+ >
170
+ <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
171
+ <Stack.Screen name="(auth)" options={{ headerShown: false }} />
172
+ </Stack>
173
+ );
174
+ }
175
+ ```
176
+
177
+ ### Paso 4: Crear store de autenticacion
178
+
179
+ Crear `stores/authStore.ts`:
180
+
181
+ ```typescript
182
+ import { create } from "zustand";
183
+ import { persist, createJSONStorage } from "zustand/middleware";
184
+ import AsyncStorage from "@react-native-async-storage/async-storage";
185
+
186
+ interface User {
187
+ id: string;
188
+ email: string;
189
+ name: string;
190
+ }
191
+
192
+ interface AuthState {
193
+ user: User | null;
194
+ token: string | null;
195
+ isLoading: boolean;
196
+ login: (email: string, password: string) => Promise<void>;
197
+ logout: () => void;
198
+ setUser: (user: User) => void;
199
+ }
200
+
201
+ export const useAuthStore = create<AuthState>()(
202
+ persist(
203
+ (set) => ({
204
+ user: null,
205
+ token: null,
206
+ isLoading: false,
207
+
208
+ login: async (email, password) => {
209
+ set({ isLoading: true });
210
+ try {
211
+ // Llamar a tu API
212
+ const response = await fetch("https://api.example.com/login", {
213
+ method: "POST",
214
+ headers: { "Content-Type": "application/json" },
215
+ body: JSON.stringify({ email, password }),
216
+ });
217
+ const data = await response.json();
218
+
219
+ set({
220
+ user: data.user,
221
+ token: data.token,
222
+ isLoading: false,
223
+ });
224
+ } catch (error) {
225
+ set({ isLoading: false });
226
+ throw error;
227
+ }
228
+ },
229
+
230
+ logout: () => {
231
+ set({ user: null, token: null });
232
+ },
233
+
234
+ setUser: (user) => set({ user }),
235
+ }),
236
+ {
237
+ name: "auth-storage",
238
+ storage: createJSONStorage(() => AsyncStorage),
239
+ }
240
+ )
241
+ );
242
+ ```
243
+
244
+ ### Paso 5: Crear componentes UI
245
+
246
+ Crear `components/ui/Button.tsx`:
247
+
248
+ ```typescript
249
+ import { TouchableOpacity, Text, ActivityIndicator } from "react-native";
250
+
251
+ interface ButtonProps {
252
+ title: string;
253
+ onPress: () => void;
254
+ variant?: "primary" | "secondary" | "outline";
255
+ loading?: boolean;
256
+ disabled?: boolean;
257
+ }
258
+
259
+ export function Button({
260
+ title,
261
+ onPress,
262
+ variant = "primary",
263
+ loading = false,
264
+ disabled = false,
265
+ }: ButtonProps) {
266
+ const baseClasses = "py-3 px-6 rounded-xl items-center justify-center";
267
+
268
+ const variantClasses = {
269
+ primary: "bg-primary",
270
+ secondary: "bg-secondary",
271
+ outline: "border-2 border-primary bg-transparent",
272
+ };
273
+
274
+ const textClasses = {
275
+ primary: "text-white font-semibold text-base",
276
+ secondary: "text-white font-semibold text-base",
277
+ outline: "text-primary font-semibold text-base",
278
+ };
279
+
280
+ return (
281
+ <TouchableOpacity
282
+ className={`${baseClasses} ${variantClasses[variant]} ${
283
+ disabled ? "opacity-50" : ""
284
+ }`}
285
+ onPress={onPress}
286
+ disabled={disabled || loading}
287
+ activeOpacity={0.8}
288
+ >
289
+ {loading ? (
290
+ <ActivityIndicator color={variant === "outline" ? "#007AFF" : "#fff"} />
291
+ ) : (
292
+ <Text className={textClasses[variant]}>{title}</Text>
293
+ )}
294
+ </TouchableOpacity>
295
+ );
296
+ }
297
+ ```
298
+
299
+ Crear `components/ui/Input.tsx`:
300
+
301
+ ```typescript
302
+ import { View, TextInput, Text } from "react-native";
303
+ import { useState } from "react";
304
+
305
+ interface InputProps {
306
+ label?: string;
307
+ placeholder?: string;
308
+ value: string;
309
+ onChangeText: (text: string) => void;
310
+ secureTextEntry?: boolean;
311
+ error?: string;
312
+ keyboardType?: "default" | "email-address" | "numeric";
313
+ }
314
+
315
+ export function Input({
316
+ label,
317
+ placeholder,
318
+ value,
319
+ onChangeText,
320
+ secureTextEntry = false,
321
+ error,
322
+ keyboardType = "default",
323
+ }: InputProps) {
324
+ const [isFocused, setIsFocused] = useState(false);
325
+
326
+ return (
327
+ <View className="mb-4">
328
+ {label && (
329
+ <Text className="text-gray-700 font-medium mb-1 dark:text-gray-300">
330
+ {label}
331
+ </Text>
332
+ )}
333
+ <TextInput
334
+ className={`bg-gray-100 dark:bg-gray-800 px-4 py-3 rounded-xl text-base ${
335
+ isFocused ? "border-2 border-primary" : "border-2 border-transparent"
336
+ } ${error ? "border-red-500" : ""}`}
337
+ placeholder={placeholder}
338
+ placeholderTextColor="#9CA3AF"
339
+ value={value}
340
+ onChangeText={onChangeText}
341
+ secureTextEntry={secureTextEntry}
342
+ keyboardType={keyboardType}
343
+ onFocus={() => setIsFocused(true)}
344
+ onBlur={() => setIsFocused(false)}
345
+ />
346
+ {error && <Text className="text-red-500 text-sm mt-1">{error}</Text>}
347
+ </View>
348
+ );
349
+ }
350
+ ```
351
+
352
+ ### Paso 6: Crear pantalla de login
353
+
354
+ Crear `app/(auth)/login.tsx`:
355
+
356
+ ```typescript
357
+ import { View, Text, KeyboardAvoidingView, Platform } from "react-native";
358
+ import { useState } from "react";
359
+ import { router } from "expo-router";
360
+ import { useAuthStore } from "@/stores/authStore";
361
+ import { Button } from "@/components/ui/Button";
362
+ import { Input } from "@/components/ui/Input";
363
+
364
+ export default function LoginScreen() {
365
+ const [email, setEmail] = useState("");
366
+ const [password, setPassword] = useState("");
367
+ const [error, setError] = useState("");
368
+
369
+ const { login, isLoading } = useAuthStore();
370
+
371
+ const handleLogin = async () => {
372
+ try {
373
+ setError("");
374
+ await login(email, password);
375
+ router.replace("/(tabs)");
376
+ } catch (err) {
377
+ setError("Credenciales invalidas");
378
+ }
379
+ };
380
+
381
+ return (
382
+ <KeyboardAvoidingView
383
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
384
+ className="flex-1 bg-white dark:bg-gray-900"
385
+ >
386
+ <View className="flex-1 justify-center px-6">
387
+ <Text className="text-3xl font-bold text-center mb-8 dark:text-white">
388
+ Bienvenido
389
+ </Text>
390
+
391
+ {error && (
392
+ <View className="bg-red-100 p-3 rounded-lg mb-4">
393
+ <Text className="text-red-600 text-center">{error}</Text>
394
+ </View>
395
+ )}
396
+
397
+ <Input
398
+ label="Email"
399
+ placeholder="tu@email.com"
400
+ value={email}
401
+ onChangeText={setEmail}
402
+ keyboardType="email-address"
403
+ />
404
+
405
+ <Input
406
+ label="Password"
407
+ placeholder="********"
408
+ value={password}
409
+ onChangeText={setPassword}
410
+ secureTextEntry
411
+ />
412
+
413
+ <View className="mt-6">
414
+ <Button
415
+ title="Iniciar Sesion"
416
+ onPress={handleLogin}
417
+ loading={isLoading}
418
+ />
419
+ </View>
420
+
421
+ <Text
422
+ className="text-center mt-4 text-gray-600 dark:text-gray-400"
423
+ onPress={() => router.push("/(auth)/register")}
424
+ >
425
+ No tienes cuenta?{" "}
426
+ <Text className="text-primary font-semibold">Registrate</Text>
427
+ </Text>
428
+ </View>
429
+ </KeyboardAvoidingView>
430
+ );
431
+ }
432
+ ```
433
+
434
+ ### Paso 7: Configurar push notifications
435
+
436
+ Crear `hooks/useNotifications.ts`:
437
+
438
+ ```typescript
439
+ import { useEffect, useRef, useState } from "react";
440
+ import * as Notifications from "expo-notifications";
441
+ import * as Device from "expo-device";
442
+ import Constants from "expo-constants";
443
+ import { Platform } from "react-native";
444
+
445
+ Notifications.setNotificationHandler({
446
+ handleNotification: async () => ({
447
+ shouldShowAlert: true,
448
+ shouldPlaySound: true,
449
+ shouldSetBadge: true,
450
+ }),
451
+ });
452
+
453
+ export function useNotifications() {
454
+ const [expoPushToken, setExpoPushToken] = useState<string | null>(null);
455
+ const [notification, setNotification] =
456
+ useState<Notifications.Notification | null>(null);
457
+ const notificationListener = useRef<Notifications.Subscription>();
458
+ const responseListener = useRef<Notifications.Subscription>();
459
+
460
+ useEffect(() => {
461
+ registerForPushNotificationsAsync().then((token) => {
462
+ if (token) setExpoPushToken(token);
463
+ });
464
+
465
+ notificationListener.current =
466
+ Notifications.addNotificationReceivedListener((notification) => {
467
+ setNotification(notification);
468
+ });
469
+
470
+ responseListener.current =
471
+ Notifications.addNotificationResponseReceivedListener((response) => {
472
+ // Manejar cuando el usuario toca la notificacion
473
+ console.log(response);
474
+ });
475
+
476
+ return () => {
477
+ if (notificationListener.current) {
478
+ Notifications.removeNotificationSubscription(
479
+ notificationListener.current
480
+ );
481
+ }
482
+ if (responseListener.current) {
483
+ Notifications.removeNotificationSubscription(responseListener.current);
484
+ }
485
+ };
486
+ }, []);
487
+
488
+ return { expoPushToken, notification };
489
+ }
490
+
491
+ async function registerForPushNotificationsAsync() {
492
+ let token;
493
+
494
+ if (Platform.OS === "android") {
495
+ await Notifications.setNotificationChannelAsync("default", {
496
+ name: "default",
497
+ importance: Notifications.AndroidImportance.MAX,
498
+ vibrationPattern: [0, 250, 250, 250],
499
+ lightColor: "#FF231F7C",
500
+ });
501
+ }
502
+
503
+ if (Device.isDevice) {
504
+ const { status: existingStatus } =
505
+ await Notifications.getPermissionsAsync();
506
+ let finalStatus = existingStatus;
507
+
508
+ if (existingStatus !== "granted") {
509
+ const { status } = await Notifications.requestPermissionsAsync();
510
+ finalStatus = status;
511
+ }
512
+
513
+ if (finalStatus !== "granted") {
514
+ console.log("Failed to get push token");
515
+ return;
516
+ }
517
+
518
+ const projectId = Constants.expoConfig?.extra?.eas?.projectId;
519
+ token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
520
+ }
521
+
522
+ return token;
523
+ }
524
+
525
+ // Funcion para enviar notificacion local
526
+ export async function sendLocalNotification(
527
+ title: string,
528
+ body: string,
529
+ data?: Record<string, unknown>
530
+ ) {
531
+ await Notifications.scheduleNotificationAsync({
532
+ content: {
533
+ title,
534
+ body,
535
+ data,
536
+ },
537
+ trigger: null, // Inmediato
538
+ });
539
+ }
540
+ ```
541
+
542
+ ### Paso 8: Configurar deep linking
543
+
544
+ Actualizar `app.json`:
545
+
546
+ ```json
547
+ {
548
+ "expo": {
549
+ "name": "my-mobile-app",
550
+ "slug": "my-mobile-app",
551
+ "scheme": "myapp",
552
+ "ios": {
553
+ "bundleIdentifier": "com.yourcompany.myapp",
554
+ "associatedDomains": ["applinks:yourapp.com"]
555
+ },
556
+ "android": {
557
+ "package": "com.yourcompany.myapp",
558
+ "intentFilters": [
559
+ {
560
+ "action": "VIEW",
561
+ "autoVerify": true,
562
+ "data": [
563
+ {
564
+ "scheme": "https",
565
+ "host": "yourapp.com",
566
+ "pathPrefix": "/"
567
+ }
568
+ ],
569
+ "category": ["BROWSABLE", "DEFAULT"]
570
+ }
571
+ ]
572
+ }
573
+ }
574
+ }
575
+ ```
576
+
577
+ Crear `app/[id].tsx` para rutas dinamicas:
578
+
579
+ ```typescript
580
+ import { View, Text } from "react-native";
581
+ import { useLocalSearchParams } from "expo-router";
582
+
583
+ export default function DetailScreen() {
584
+ const { id } = useLocalSearchParams();
585
+
586
+ return (
587
+ <View className="flex-1 items-center justify-center bg-white dark:bg-gray-900">
588
+ <Text className="text-xl dark:text-white">Detalle: {id}</Text>
589
+ </View>
590
+ );
591
+ }
592
+ ```
593
+
594
+ ### Paso 9: Crear servicio de API
595
+
596
+ Crear `services/api.ts`:
597
+
598
+ ```typescript
599
+ const API_URL = "https://api.example.com";
600
+
601
+ class ApiService {
602
+ private token: string | null = null;
603
+
604
+ setToken(token: string | null) {
605
+ this.token = token;
606
+ }
607
+
608
+ private async request<T>(
609
+ endpoint: string,
610
+ options: RequestInit = {}
611
+ ): Promise<T> {
612
+ const headers: HeadersInit = {
613
+ "Content-Type": "application/json",
614
+ ...options.headers,
615
+ };
616
+
617
+ if (this.token) {
618
+ headers["Authorization"] = `Bearer ${this.token}`;
619
+ }
620
+
621
+ const response = await fetch(`${API_URL}${endpoint}`, {
622
+ ...options,
623
+ headers,
624
+ });
625
+
626
+ if (!response.ok) {
627
+ throw new Error(`API Error: ${response.status}`);
628
+ }
629
+
630
+ return response.json();
631
+ }
632
+
633
+ // Metodos de la API
634
+ async getItems() {
635
+ return this.request<Item[]>("/items");
636
+ }
637
+
638
+ async getItem(id: string) {
639
+ return this.request<Item>(`/items/${id}`);
640
+ }
641
+
642
+ async createItem(data: CreateItemInput) {
643
+ return this.request<Item>("/items", {
644
+ method: "POST",
645
+ body: JSON.stringify(data),
646
+ });
647
+ }
648
+ }
649
+
650
+ export const api = new ApiService();
651
+
652
+ // Types
653
+ interface Item {
654
+ id: string;
655
+ title: string;
656
+ description: string;
657
+ }
658
+
659
+ interface CreateItemInput {
660
+ title: string;
661
+ description: string;
662
+ }
663
+ ```
664
+
665
+ ### Paso 10: Tab Home con lista
666
+
667
+ Actualizar `app/(tabs)/index.tsx`:
668
+
669
+ ```typescript
670
+ import { View, Text, FlatList, RefreshControl } from "react-native";
671
+ import { useState, useCallback } from "react";
672
+ import { Button } from "@/components/ui/Button";
673
+ import { router } from "expo-router";
674
+
675
+ interface Item {
676
+ id: string;
677
+ title: string;
678
+ description: string;
679
+ }
680
+
681
+ export default function HomeScreen() {
682
+ const [items, setItems] = useState<Item[]>([
683
+ { id: "1", title: "Primer item", description: "Descripcion del item" },
684
+ { id: "2", title: "Segundo item", description: "Otra descripcion" },
685
+ ]);
686
+ const [refreshing, setRefreshing] = useState(false);
687
+
688
+ const onRefresh = useCallback(async () => {
689
+ setRefreshing(true);
690
+ // Simular fetch
691
+ await new Promise((resolve) => setTimeout(resolve, 1000));
692
+ setRefreshing(false);
693
+ }, []);
694
+
695
+ const renderItem = ({ item }: { item: Item }) => (
696
+ <View
697
+ className="bg-white dark:bg-gray-800 p-4 rounded-xl mb-3 shadow-sm"
698
+ onTouchEnd={() => router.push(`/${item.id}`)}
699
+ >
700
+ <Text className="text-lg font-semibold dark:text-white">
701
+ {item.title}
702
+ </Text>
703
+ <Text className="text-gray-600 dark:text-gray-400 mt-1">
704
+ {item.description}
705
+ </Text>
706
+ </View>
707
+ );
708
+
709
+ return (
710
+ <View className="flex-1 bg-gray-50 dark:bg-gray-900">
711
+ <FlatList
712
+ data={items}
713
+ renderItem={renderItem}
714
+ keyExtractor={(item) => item.id}
715
+ contentContainerClassName="p-4"
716
+ refreshControl={
717
+ <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
718
+ }
719
+ ListEmptyComponent={
720
+ <View className="items-center py-8">
721
+ <Text className="text-gray-500">No hay items</Text>
722
+ </View>
723
+ }
724
+ />
725
+
726
+ <View className="p-4">
727
+ <Button
728
+ title="Agregar Item"
729
+ onPress={() => console.log("Add item")}
730
+ />
731
+ </View>
732
+ </View>
733
+ );
734
+ }
735
+ ```
736
+ </setup_steps>
737
+
738
+ <verification>
739
+ ## Verificacion
740
+
741
+ ### 1. Iniciar app
742
+ ```bash
743
+ npx expo start
744
+ ```
745
+
746
+ ### 2. Probar en Expo Go
747
+ - Escanear QR con Expo Go
748
+ - Navegar entre tabs
749
+ - Probar dark mode
750
+
751
+ ### 3. Verificar NativeWind
752
+ - Los estilos de Tailwind deben aplicar
753
+ - Dark mode debe funcionar
754
+
755
+ ### 4. Probar deep link
756
+ ```bash
757
+ # iOS Simulator
758
+ npx uri-scheme open myapp://1 --ios
759
+
760
+ # Android Emulator
761
+ npx uri-scheme open myapp://1 --android
762
+ ```
763
+
764
+ ### 5. Build de desarrollo
765
+ ```bash
766
+ npx expo run:ios
767
+ # o
768
+ npx expo run:android
769
+ ```
770
+ </verification>
771
+
772
+ <common_issues>
773
+ ## Problemas Comunes
774
+
775
+ ### "NativeWind styles not applying"
776
+ - Verificar babel.config.js
777
+ - Reiniciar metro bundler: `npx expo start -c`
778
+
779
+ ### "Push notifications not working in Expo Go"
780
+ - Las push notifications requieren build nativo
781
+ - Usar `expo-dev-client` para testing completo
782
+
783
+ ### "Deep links not working"
784
+ - Verificar scheme en app.json
785
+ - Reinstalar la app despues de cambiar config
786
+ </common_issues>
787
+
788
+ <builds>
789
+ ## Builds para Stores
790
+
791
+ ### Configurar EAS Build
792
+ ```bash
793
+ npm install -g eas-cli
794
+ eas login
795
+ eas build:configure
796
+ ```
797
+
798
+ ### Build iOS
799
+ ```bash
800
+ eas build --platform ios
801
+ ```
802
+
803
+ ### Build Android
804
+ ```bash
805
+ eas build --platform android
806
+ ```
807
+
808
+ ### Submit a stores
809
+ ```bash
810
+ eas submit --platform ios
811
+ eas submit --platform android
812
+ ```
813
+ </builds>