@the-bearded-bear/claude-craft 8.8.1 → 8.8.2
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/Dev/i18n/de/Angular/commands/check-compliance.md +218 -168
- package/Dev/i18n/de/Common/agents/refactoring-specialist.md +146 -69
- package/Dev/i18n/de/Common/commands/add-technology.md +166 -68
- package/Dev/i18n/de/Common/commands/setup-ci.md +216 -33
- package/Dev/i18n/de/Laravel/commands/check-compliance.md +223 -115
- package/Dev/i18n/de/Python/checklists/new-feature.md +319 -44
- package/Dev/i18n/de/React/rules/02-architecture.md +483 -322
- package/Dev/i18n/de/React/rules/03-coding-standards.md +584 -368
- package/Dev/i18n/de/Symfony/commands/check-compliance.md +186 -114
- package/Dev/i18n/de/Symfony/rules/07-testing-symfony.md +679 -10
- package/Dev/i18n/de/UIUX/commands/a11y-component.md +284 -26
- package/Dev/i18n/de/UIUX/commands/design-tokens.md +199 -35
- package/Dev/i18n/de/UIUX/commands/user-flow.md +179 -16
- package/Dev/i18n/de/VueJS/commands/check-compliance.md +227 -102
- package/Dev/i18n/es/Angular/commands/check-compliance.md +217 -167
- package/Dev/i18n/es/Common/commands/add-technology.md +158 -60
- package/Dev/i18n/es/Laravel/commands/check-compliance.md +223 -115
- package/Dev/i18n/es/React/commands/check-testing.md +263 -169
- package/Dev/i18n/es/ReactNative/commands/check-testing.md +256 -198
- package/Dev/i18n/es/Symfony/commands/check-compliance.md +189 -117
- package/Dev/i18n/es/Symfony/commands/check-testing.md +301 -102
- package/Dev/i18n/es/Symfony/commands/migration-plan.md +267 -34
- package/Dev/i18n/es/UIUX/commands/a11y-component.md +274 -16
- package/Dev/i18n/es/UIUX/commands/design-tokens.md +186 -22
- package/Dev/i18n/es/UIUX/commands/user-flow.md +173 -10
- package/Dev/i18n/es/VueJS/commands/check-compliance.md +227 -102
- package/Dev/i18n/fr/Angular/commands/check-compliance.md +217 -167
- package/Dev/i18n/fr/Common/commands/ralph-run.md +138 -60
- package/Dev/i18n/fr/Laravel/commands/check-compliance.md +223 -115
- package/Dev/i18n/fr/React/commands/check-architecture.md +370 -53
- package/Dev/i18n/fr/React/commands/check-code-quality.md +454 -64
- package/Dev/i18n/fr/React/commands/check-testing.md +476 -102
- package/Dev/i18n/fr/Symfony/commands/check-compliance.md +188 -116
- package/Dev/i18n/fr/VueJS/commands/check-compliance.md +227 -102
- package/Dev/i18n/pt/Angular/commands/check-compliance.md +225 -175
- package/Dev/i18n/pt/Common/agents/ralph-conductor.md +211 -30
- package/Dev/i18n/pt/Common/commands/add-technology.md +149 -51
- package/Dev/i18n/pt/Common/commands/ralph-run.md +239 -35
- package/Dev/i18n/pt/Flutter/commands/analyze-performance.md +331 -46
- package/Dev/i18n/pt/Flutter/commands/check-compliance.md +234 -29
- package/Dev/i18n/pt/Flutter/commands/generate-feature.md +526 -51
- package/Dev/i18n/pt/Flutter/commands/generate-widget.md +515 -27
- package/Dev/i18n/pt/Flutter/commands/golden-update.md +322 -46
- package/Dev/i18n/pt/Flutter/commands/localization-check.md +278 -37
- package/Dev/i18n/pt/Laravel/commands/check-compliance.md +212 -104
- package/Dev/i18n/pt/React/commands/accessibility-check.md +151 -42
- package/Dev/i18n/pt/React/commands/bundle-analyze.md +300 -25
- package/Dev/i18n/pt/React/commands/check-architecture.md +369 -34
- package/Dev/i18n/pt/React/commands/check-code-quality.md +440 -34
- package/Dev/i18n/pt/React/commands/check-compliance.md +232 -62
- package/Dev/i18n/pt/React/commands/check-security.md +370 -31
- package/Dev/i18n/pt/React/commands/check-testing.md +473 -43
- package/Dev/i18n/pt/React/commands/generate-component.md +454 -19
- package/Dev/i18n/pt/React/commands/generate-hook.md +425 -24
- package/Dev/i18n/pt/React/commands/storybook-story.md +520 -35
- package/Dev/i18n/pt/ReactNative/commands/app-size.md +358 -387
- package/Dev/i18n/pt/ReactNative/commands/check-code-quality.md +246 -75
- package/Dev/i18n/pt/ReactNative/commands/check-compliance.md +191 -85
- package/Dev/i18n/pt/ReactNative/commands/check-security.md +363 -115
- package/Dev/i18n/pt/ReactNative/commands/check-testing.md +269 -79
- package/Dev/i18n/pt/ReactNative/commands/generate-screen.md +491 -324
- package/Dev/i18n/pt/ReactNative/commands/native-module.md +539 -82
- package/Dev/i18n/pt/ReactNative/commands/store-prepare.md +429 -185
- package/Dev/i18n/pt/ReactNative/rules/02-architecture.md +1084 -31
- package/Dev/i18n/pt/Symfony/commands/check-compliance.md +195 -123
- package/Dev/i18n/pt/UIUX/commands/a11y-audit.md +208 -24
- package/Dev/i18n/pt/UIUX/commands/a11y-component.md +281 -23
- package/Dev/i18n/pt/UIUX/commands/design-tokens.md +197 -33
- package/Dev/i18n/pt/UIUX/commands/user-flow.md +183 -20
- package/Dev/i18n/pt/VueJS/commands/check-compliance.md +228 -103
- package/Dev/i18n/pt/Workflow/commands/analyze.md +147 -146
- package/Dev/i18n/pt/Workflow/commands/design.md +205 -204
- package/Dev/i18n/pt/Workflow/commands/implement.md +184 -184
- package/Project/i18n/de/Sprint/commands/dev.md +339 -86
- package/Project/i18n/es/Sprint/commands/dev.md +342 -91
- package/Project/i18n/pt/Sprint/commands/dev.md +343 -92
- package/bundles/cursor/.cursorrules +1 -1
- package/bundles/windsurf/.windsurfrules +1 -1
- package/package.json +1 -1
|
@@ -1,420 +1,587 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Geração de Tela React Native
|
|
3
|
+
argument-hint: [arguments]
|
|
3
4
|
---
|
|
4
5
|
|
|
5
|
-
#
|
|
6
|
+
# Geração de Tela React Native
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Você é um desenvolvedor React Native sênior. Você deve gerar uma tela completa com navegação, gerenciamento de estado, testes e acessibilidade.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## Uso
|
|
10
|
+
## Argumentos
|
|
11
|
+
$ARGUMENTS
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
Argumentos:
|
|
14
|
+
- Nome da tela (ex.: `Profile`, `Settings`, `ProductDetail`)
|
|
15
|
+
- (Opcional) Tipo: list, detail, form, dashboard
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
- `--type`: tipo de screen (stack, tab, modal)
|
|
19
|
-
- `--feature`: feature a qual pertence
|
|
20
|
-
- `--with-test`: gerar arquivo de teste
|
|
21
|
-
- `--with-form`: incluir componentes de formulário
|
|
17
|
+
Exemplo: `/reactnative:generate-screen ProductDetail detail`
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
## Modo de Planejamento
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
# Screen simples
|
|
27
|
-
npx generate-screen UserProfile
|
|
21
|
+
> **O modo de planejamento é obrigatório.** Antes de executar, o Claude ativa o modo de planejamento para analisar o código impactado, propor um plano de implementação e aguardar sua validação antes de realizar qualquer alteração.
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
npx generate-screen CreatePost --feature=posts
|
|
23
|
+
## MISSÃO
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
npx generate-screen Settings --with-test
|
|
25
|
+
### Etapa 1: Estrutura da Tela
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
```
|
|
28
|
+
src/
|
|
29
|
+
└── screens/
|
|
30
|
+
└── {ScreenName}/
|
|
31
|
+
├── index.ts
|
|
32
|
+
├── {ScreenName}Screen.tsx
|
|
33
|
+
├── {ScreenName}Screen.types.ts
|
|
34
|
+
├── {ScreenName}Screen.styles.ts
|
|
35
|
+
├── {ScreenName}Screen.test.tsx
|
|
36
|
+
├── hooks/
|
|
37
|
+
│ └── use{ScreenName}.ts
|
|
38
|
+
└── components/
|
|
39
|
+
└── {ScreenName}Header.tsx
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
## Modo Plano
|
|
42
|
+
### Etapa 2: Tipos
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
```typescript
|
|
45
|
+
// src/screens/{ScreenName}/{ScreenName}Screen.types.ts
|
|
46
|
+
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
|
47
|
+
import type { RootStackParamList } from '@/navigation/types';
|
|
48
|
+
|
|
49
|
+
export type {ScreenName}ScreenProps = NativeStackScreenProps<
|
|
50
|
+
RootStackParamList,
|
|
51
|
+
'{ScreenName}'
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
export interface {ScreenName}ScreenParams {
|
|
55
|
+
id: string;
|
|
56
|
+
title?: string;
|
|
57
|
+
}
|
|
44
58
|
|
|
45
|
-
|
|
59
|
+
export interface {ScreenName}Data {
|
|
60
|
+
id: string;
|
|
61
|
+
title: string;
|
|
62
|
+
description: string;
|
|
63
|
+
imageUrl?: string;
|
|
64
|
+
createdAt: string;
|
|
65
|
+
// ...outros campos
|
|
66
|
+
}
|
|
46
67
|
|
|
47
|
-
|
|
68
|
+
export interface {ScreenName}ScreenState {
|
|
69
|
+
data: {ScreenName}Data | null;
|
|
70
|
+
isLoading: boolean;
|
|
71
|
+
error: string | null;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
48
74
|
|
|
49
|
-
|
|
75
|
+
### Etapa 3: Hook Personalizado
|
|
50
76
|
|
|
51
77
|
```typescript
|
|
52
|
-
|
|
53
|
-
import {
|
|
78
|
+
// src/screens/{ScreenName}/hooks/use{ScreenName}.ts
|
|
79
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
80
|
+
import { useRoute } from '@react-navigation/native';
|
|
81
|
+
|
|
82
|
+
import { {screenName}Service } from '@/services/{screenName}Service';
|
|
83
|
+
import type { {ScreenName}Data, {ScreenName}ScreenParams } from '../{ScreenName}Screen.types';
|
|
84
|
+
|
|
85
|
+
interface Use{ScreenName}Return {
|
|
86
|
+
data: {ScreenName}Data | null;
|
|
87
|
+
isLoading: boolean;
|
|
88
|
+
error: string | null;
|
|
89
|
+
refresh: () => Promise<void>;
|
|
90
|
+
}
|
|
54
91
|
|
|
55
|
-
export
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
92
|
+
export function use{ScreenName}(): Use{ScreenName}Return {
|
|
93
|
+
const route = useRoute<{ params: {ScreenName}ScreenParams }>();
|
|
94
|
+
const { id } = route.params;
|
|
95
|
+
|
|
96
|
+
const [data, setData] = useState<{ScreenName}Data | null>(null);
|
|
97
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
98
|
+
const [error, setError] = useState<string | null>(null);
|
|
99
|
+
|
|
100
|
+
const fetchData = useCallback(async () => {
|
|
101
|
+
try {
|
|
102
|
+
setIsLoading(true);
|
|
103
|
+
setError(null);
|
|
104
|
+
const result = await {screenName}Service.getById(id);
|
|
105
|
+
setData(result);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
setError(err instanceof Error ? err.message : 'Ocorreu um erro');
|
|
108
|
+
} finally {
|
|
109
|
+
setIsLoading(false);
|
|
110
|
+
}
|
|
111
|
+
}, [id]);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
fetchData();
|
|
115
|
+
}, [fetchData]);
|
|
116
|
+
|
|
117
|
+
const refresh = useCallback(async () => {
|
|
118
|
+
await fetchData();
|
|
119
|
+
}, [fetchData]);
|
|
120
|
+
|
|
121
|
+
return { data, isLoading, error, refresh };
|
|
63
122
|
}
|
|
123
|
+
```
|
|
64
124
|
|
|
65
|
-
|
|
125
|
+
### Etapa 4: Estilos
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// src/screens/{ScreenName}/{ScreenName}Screen.styles.ts
|
|
129
|
+
import { StyleSheet } from 'react-native';
|
|
130
|
+
import { theme } from '@/theme';
|
|
131
|
+
|
|
132
|
+
export const styles = StyleSheet.create({
|
|
66
133
|
container: {
|
|
67
134
|
flex: 1,
|
|
68
|
-
backgroundColor:
|
|
135
|
+
backgroundColor: theme.colors.background,
|
|
69
136
|
},
|
|
70
137
|
content: {
|
|
71
138
|
flex: 1,
|
|
72
|
-
padding:
|
|
139
|
+
padding: theme.spacing.md,
|
|
140
|
+
},
|
|
141
|
+
loadingContainer: {
|
|
142
|
+
flex: 1,
|
|
143
|
+
justifyContent: 'center',
|
|
144
|
+
alignItems: 'center',
|
|
145
|
+
},
|
|
146
|
+
errorContainer: {
|
|
147
|
+
flex: 1,
|
|
148
|
+
justifyContent: 'center',
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
padding: theme.spacing.lg,
|
|
151
|
+
},
|
|
152
|
+
errorText: {
|
|
153
|
+
fontSize: theme.typography.body.fontSize,
|
|
154
|
+
color: theme.colors.error,
|
|
155
|
+
textAlign: 'center',
|
|
156
|
+
marginBottom: theme.spacing.md,
|
|
157
|
+
},
|
|
158
|
+
retryButton: {
|
|
159
|
+
paddingHorizontal: theme.spacing.lg,
|
|
160
|
+
paddingVertical: theme.spacing.sm,
|
|
161
|
+
backgroundColor: theme.colors.primary,
|
|
162
|
+
borderRadius: theme.borderRadius.md,
|
|
163
|
+
},
|
|
164
|
+
retryButtonText: {
|
|
165
|
+
color: theme.colors.white,
|
|
166
|
+
fontWeight: '600',
|
|
167
|
+
},
|
|
168
|
+
headerImage: {
|
|
169
|
+
width: '100%',
|
|
170
|
+
height: 200,
|
|
171
|
+
resizeMode: 'cover',
|
|
73
172
|
},
|
|
74
173
|
title: {
|
|
75
|
-
fontSize:
|
|
76
|
-
fontWeight:
|
|
77
|
-
|
|
174
|
+
fontSize: theme.typography.h1.fontSize,
|
|
175
|
+
fontWeight: theme.typography.h1.fontWeight,
|
|
176
|
+
color: theme.colors.text,
|
|
177
|
+
marginBottom: theme.spacing.sm,
|
|
178
|
+
},
|
|
179
|
+
description: {
|
|
180
|
+
fontSize: theme.typography.body.fontSize,
|
|
181
|
+
color: theme.colors.textSecondary,
|
|
182
|
+
lineHeight: 24,
|
|
183
|
+
},
|
|
184
|
+
metadata: {
|
|
185
|
+
flexDirection: 'row',
|
|
186
|
+
alignItems: 'center',
|
|
187
|
+
marginTop: theme.spacing.md,
|
|
188
|
+
},
|
|
189
|
+
metadataText: {
|
|
190
|
+
fontSize: theme.typography.caption.fontSize,
|
|
191
|
+
color: theme.colors.textMuted,
|
|
78
192
|
},
|
|
79
193
|
});
|
|
80
194
|
```
|
|
81
195
|
|
|
82
|
-
###
|
|
196
|
+
### Etapa 5: Componente da Tela
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
// src/screens/{ScreenName}/{ScreenName}Screen.tsx
|
|
200
|
+
import React, { useCallback } from 'react';
|
|
201
|
+
import {
|
|
202
|
+
View,
|
|
203
|
+
Text,
|
|
204
|
+
ScrollView,
|
|
205
|
+
Image,
|
|
206
|
+
RefreshControl,
|
|
207
|
+
TouchableOpacity,
|
|
208
|
+
ActivityIndicator,
|
|
209
|
+
} from 'react-native';
|
|
210
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
83
211
|
|
|
84
|
-
|
|
212
|
+
import { use{ScreenName} } from './hooks/use{ScreenName}';
|
|
213
|
+
import { styles } from './{ScreenName}Screen.styles';
|
|
214
|
+
import type { {ScreenName}ScreenProps } from './{ScreenName}Screen.types';
|
|
85
215
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
90
|
-
import { useArticle } from '@/features/articles/hooks/useArticle';
|
|
216
|
+
export function {ScreenName}Screen({ navigation }: {ScreenName}ScreenProps) {
|
|
217
|
+
const { data, isLoading, error, refresh } = use{ScreenName}();
|
|
218
|
+
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
|
91
219
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
220
|
+
const handleRefresh = useCallback(async () => {
|
|
221
|
+
setIsRefreshing(true);
|
|
222
|
+
await refresh();
|
|
223
|
+
setIsRefreshing(false);
|
|
224
|
+
}, [refresh]);
|
|
95
225
|
|
|
96
|
-
|
|
226
|
+
// Estado de carregamento inicial
|
|
227
|
+
if (isLoading && !data) {
|
|
97
228
|
return (
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
229
|
+
<SafeAreaView style={styles.container}>
|
|
230
|
+
<View
|
|
231
|
+
style={styles.loadingContainer}
|
|
232
|
+
accessibilityLabel="Carregamento em andamento"
|
|
233
|
+
accessibilityRole="progressbar"
|
|
234
|
+
>
|
|
235
|
+
<ActivityIndicator size="large" />
|
|
236
|
+
</View>
|
|
237
|
+
</SafeAreaView>
|
|
101
238
|
);
|
|
102
239
|
}
|
|
103
240
|
|
|
104
|
-
|
|
241
|
+
// Estado de erro
|
|
242
|
+
if (error && !data) {
|
|
105
243
|
return (
|
|
106
|
-
<
|
|
107
|
-
<
|
|
108
|
-
|
|
244
|
+
<SafeAreaView style={styles.container}>
|
|
245
|
+
<View style={styles.errorContainer}>
|
|
246
|
+
<Text
|
|
247
|
+
style={styles.errorText}
|
|
248
|
+
accessibilityRole="alert"
|
|
249
|
+
>
|
|
250
|
+
{error}
|
|
251
|
+
</Text>
|
|
252
|
+
<TouchableOpacity
|
|
253
|
+
style={styles.retryButton}
|
|
254
|
+
onPress={refresh}
|
|
255
|
+
accessibilityLabel="Tentar novamente o carregamento"
|
|
256
|
+
accessibilityRole="button"
|
|
257
|
+
>
|
|
258
|
+
<Text style={styles.retryButtonText}>Tentar novamente</Text>
|
|
259
|
+
</TouchableOpacity>
|
|
260
|
+
</View>
|
|
261
|
+
</SafeAreaView>
|
|
109
262
|
);
|
|
110
263
|
}
|
|
111
264
|
|
|
265
|
+
// Estado normal com dados
|
|
112
266
|
return (
|
|
113
|
-
<SafeAreaView style={styles.container}>
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
267
|
+
<SafeAreaView style={styles.container} edges={['bottom']}>
|
|
268
|
+
<ScrollView
|
|
269
|
+
style={styles.content}
|
|
270
|
+
refreshControl={
|
|
271
|
+
<RefreshControl
|
|
272
|
+
refreshing={isRefreshing}
|
|
273
|
+
onRefresh={handleRefresh}
|
|
274
|
+
accessibilityLabel="Atualizar página"
|
|
275
|
+
/>
|
|
276
|
+
}
|
|
277
|
+
contentContainerStyle={{ paddingBottom: 20 }}
|
|
278
|
+
>
|
|
279
|
+
{data?.imageUrl && (
|
|
280
|
+
<Image
|
|
281
|
+
source={{ uri: data.imageUrl }}
|
|
282
|
+
style={styles.headerImage}
|
|
283
|
+
accessibilityLabel={`Imagem de ${data.title}`}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
<View style={{ padding: 16 }}>
|
|
288
|
+
<Text
|
|
289
|
+
style={styles.title}
|
|
290
|
+
accessibilityRole="header"
|
|
291
|
+
>
|
|
292
|
+
{data?.title}
|
|
293
|
+
</Text>
|
|
294
|
+
|
|
295
|
+
<Text style={styles.description}>
|
|
296
|
+
{data?.description}
|
|
297
|
+
</Text>
|
|
298
|
+
|
|
299
|
+
<View style={styles.metadata}>
|
|
300
|
+
<Text style={styles.metadataText}>
|
|
301
|
+
Criado em {new Date(data?.createdAt || '').toLocaleDateString()}
|
|
302
|
+
</Text>
|
|
303
|
+
</View>
|
|
304
|
+
</View>
|
|
305
|
+
</ScrollView>
|
|
118
306
|
</SafeAreaView>
|
|
119
307
|
);
|
|
120
308
|
}
|
|
121
|
-
|
|
122
|
-
const styles = StyleSheet.create({
|
|
123
|
-
container: {
|
|
124
|
-
flex: 1,
|
|
125
|
-
backgroundColor: '#fff',
|
|
126
|
-
},
|
|
127
|
-
centerContainer: {
|
|
128
|
-
flex: 1,
|
|
129
|
-
justifyContent: 'center',
|
|
130
|
-
alignItems: 'center',
|
|
131
|
-
},
|
|
132
|
-
content: {
|
|
133
|
-
flex: 1,
|
|
134
|
-
padding: 16,
|
|
135
|
-
},
|
|
136
|
-
title: {
|
|
137
|
-
fontSize: 24,
|
|
138
|
-
fontWeight: 'bold',
|
|
139
|
-
marginBottom: 16,
|
|
140
|
-
},
|
|
141
|
-
body: {
|
|
142
|
-
fontSize: 16,
|
|
143
|
-
lineHeight: 24,
|
|
144
|
-
},
|
|
145
|
-
errorText: {
|
|
146
|
-
fontSize: 16,
|
|
147
|
-
color: '#f00',
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
309
|
```
|
|
151
310
|
|
|
152
|
-
###
|
|
311
|
+
### Etapa 6: Exportação
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// src/screens/{ScreenName}/index.ts
|
|
315
|
+
export { {ScreenName}Screen } from './{ScreenName}Screen';
|
|
316
|
+
export type { {ScreenName}ScreenProps, {ScreenName}ScreenParams } from './{ScreenName}Screen.types';
|
|
317
|
+
```
|
|
153
318
|
|
|
154
|
-
|
|
319
|
+
### Etapa 7: Navegação
|
|
155
320
|
|
|
156
321
|
```typescript
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
};
|
|
322
|
+
// src/navigation/types.ts
|
|
323
|
+
export type RootStackParamList = {
|
|
324
|
+
Home: undefined;
|
|
325
|
+
{ScreenName}: { id: string; title?: string };
|
|
326
|
+
// ...outras telas
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/navigation/RootNavigator.tsx
|
|
330
|
+
import { {ScreenName}Screen } from '@/screens/{ScreenName}';
|
|
331
|
+
|
|
332
|
+
<Stack.Screen
|
|
333
|
+
name="{ScreenName}"
|
|
334
|
+
component={{ScreenName}Screen}
|
|
335
|
+
options={({ route }) => ({
|
|
336
|
+
title: route.params?.title || '{ScreenName}',
|
|
337
|
+
headerBackTitleVisible: false,
|
|
338
|
+
})}
|
|
339
|
+
/>
|
|
340
|
+
```
|
|
177
341
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
342
|
+
### Etapa 8: Testes
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
// src/screens/{ScreenName}/{ScreenName}Screen.test.tsx
|
|
346
|
+
import React from 'react';
|
|
347
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
|
|
348
|
+
import { NavigationContainer } from '@react-navigation/native';
|
|
349
|
+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
350
|
+
|
|
351
|
+
import { {ScreenName}Screen } from './{ScreenName}Screen';
|
|
352
|
+
import { {screenName}Service } from '@/services/{screenName}Service';
|
|
353
|
+
|
|
354
|
+
// Mock do serviço
|
|
355
|
+
jest.mock('@/services/{screenName}Service');
|
|
356
|
+
const mockService = {screenName}Service as jest.Mocked<typeof {screenName}Service>;
|
|
357
|
+
|
|
358
|
+
const Stack = createNativeStackNavigator();
|
|
359
|
+
|
|
360
|
+
function renderWithNavigation(params = { id: '123' }) {
|
|
361
|
+
return render(
|
|
362
|
+
<NavigationContainer>
|
|
363
|
+
<Stack.Navigator>
|
|
364
|
+
<Stack.Screen
|
|
365
|
+
name="{ScreenName}"
|
|
366
|
+
component={{ScreenName}Screen}
|
|
367
|
+
initialParams={params}
|
|
368
|
+
/>
|
|
369
|
+
</Stack.Navigator>
|
|
370
|
+
</NavigationContainer>
|
|
190
371
|
);
|
|
191
372
|
}
|
|
192
373
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
},
|
|
198
|
-
scrollView: {
|
|
199
|
-
flex: 1,
|
|
200
|
-
},
|
|
201
|
-
content: {
|
|
202
|
-
padding: 16,
|
|
203
|
-
},
|
|
204
|
-
title: {
|
|
205
|
-
fontSize: 24,
|
|
206
|
-
fontWeight: 'bold',
|
|
207
|
-
marginBottom: 24,
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
```
|
|
374
|
+
describe('{ScreenName}Screen', () => {
|
|
375
|
+
beforeEach(() => {
|
|
376
|
+
jest.clearAllMocks();
|
|
377
|
+
});
|
|
211
378
|
|
|
212
|
-
|
|
379
|
+
describe('Estado de carregamento', () => {
|
|
380
|
+
it('exibe indicador de carregamento inicialmente', () => {
|
|
381
|
+
mockService.getById.mockImplementation(() => new Promise(() => {}));
|
|
213
382
|
|
|
214
|
-
|
|
383
|
+
renderWithNavigation();
|
|
215
384
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
import { StatusBar } from 'expo-status-bar';
|
|
385
|
+
expect(screen.getByLabelText('Carregamento em andamento')).toBeTruthy();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
220
388
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
389
|
+
describe('Estado de sucesso', () => {
|
|
390
|
+
const mockData = {
|
|
391
|
+
id: '123',
|
|
392
|
+
title: 'Título de Teste',
|
|
393
|
+
description: 'Descrição de Teste',
|
|
394
|
+
createdAt: '2024-01-15T10:00:00Z',
|
|
395
|
+
};
|
|
225
396
|
|
|
226
|
-
|
|
227
|
-
|
|
397
|
+
beforeEach(() => {
|
|
398
|
+
mockService.getById.mockResolvedValue(mockData);
|
|
399
|
+
});
|
|
228
400
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
onPress={() => router.back()}
|
|
232
|
-
>
|
|
233
|
-
<Text style={styles.closeButtonText}>Fechar</Text>
|
|
234
|
-
</Pressable>
|
|
235
|
-
</View>
|
|
236
|
-
</View>
|
|
237
|
-
);
|
|
238
|
-
}
|
|
401
|
+
it('exibe os dados corretamente', async () => {
|
|
402
|
+
renderWithNavigation();
|
|
239
403
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
244
|
-
justifyContent: 'center',
|
|
245
|
-
alignItems: 'center',
|
|
246
|
-
},
|
|
247
|
-
content: {
|
|
248
|
-
backgroundColor: '#fff',
|
|
249
|
-
borderRadius: 16,
|
|
250
|
-
padding: 24,
|
|
251
|
-
width: '80%',
|
|
252
|
-
maxWidth: 400,
|
|
253
|
-
},
|
|
254
|
-
title: {
|
|
255
|
-
fontSize: 24,
|
|
256
|
-
fontWeight: 'bold',
|
|
257
|
-
marginBottom: 24,
|
|
258
|
-
textAlign: 'center',
|
|
259
|
-
},
|
|
260
|
-
closeButton: {
|
|
261
|
-
backgroundColor: '#007AFF',
|
|
262
|
-
padding: 12,
|
|
263
|
-
borderRadius: 8,
|
|
264
|
-
alignItems: 'center',
|
|
265
|
-
},
|
|
266
|
-
closeButtonText: {
|
|
267
|
-
color: '#fff',
|
|
268
|
-
fontSize: 16,
|
|
269
|
-
fontWeight: '600',
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
```
|
|
404
|
+
await waitFor(() => {
|
|
405
|
+
expect(screen.getByText('Título de Teste')).toBeTruthy();
|
|
406
|
+
});
|
|
273
407
|
|
|
274
|
-
|
|
408
|
+
expect(screen.getByText('Descrição de Teste')).toBeTruthy();
|
|
409
|
+
});
|
|
275
410
|
|
|
276
|
-
|
|
411
|
+
it('suporta pull to refresh', async () => {
|
|
412
|
+
renderWithNavigation();
|
|
277
413
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
import { useUsers } from '@/features/users/hooks/useUsers';
|
|
282
|
-
import { UserCard } from '@/features/users/components/UserCard';
|
|
283
|
-
import type { User } from '@/features/users/types/User.types';
|
|
414
|
+
await waitFor(() => {
|
|
415
|
+
expect(screen.getByText('Título de Teste')).toBeTruthy();
|
|
416
|
+
});
|
|
284
417
|
|
|
285
|
-
|
|
286
|
-
|
|
418
|
+
const scrollView = screen.getByLabelText('Atualizar página');
|
|
419
|
+
fireEvent(scrollView, 'refresh');
|
|
287
420
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
);
|
|
294
|
-
}
|
|
421
|
+
await waitFor(() => {
|
|
422
|
+
expect(mockService.getById).toHaveBeenCalledTimes(2);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
});
|
|
295
426
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
</View>
|
|
301
|
-
);
|
|
302
|
-
}
|
|
427
|
+
describe('Estado de erro', () => {
|
|
428
|
+
beforeEach(() => {
|
|
429
|
+
mockService.getById.mockRejectedValue(new Error('Erro de rede'));
|
|
430
|
+
});
|
|
303
431
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
<FlatList
|
|
307
|
-
data={users}
|
|
308
|
-
keyExtractor={(item: User) => item.id}
|
|
309
|
-
renderItem={({ item }) => <UserCard user={item} />}
|
|
310
|
-
contentContainerStyle={styles.listContent}
|
|
311
|
-
onRefresh={refetch}
|
|
312
|
-
refreshing={isLoading}
|
|
313
|
-
ListEmptyComponent={
|
|
314
|
-
<View style={styles.emptyContainer}>
|
|
315
|
-
<Text style={styles.emptyText}>Nenhum usuário encontrado</Text>
|
|
316
|
-
</View>
|
|
317
|
-
}
|
|
318
|
-
/>
|
|
319
|
-
</SafeAreaView>
|
|
320
|
-
);
|
|
321
|
-
}
|
|
432
|
+
it('exibe mensagem de erro', async () => {
|
|
433
|
+
renderWithNavigation();
|
|
322
434
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
},
|
|
328
|
-
centerContainer: {
|
|
329
|
-
flex: 1,
|
|
330
|
-
justifyContent: 'center',
|
|
331
|
-
alignItems: 'center',
|
|
332
|
-
},
|
|
333
|
-
listContent: {
|
|
334
|
-
padding: 16,
|
|
335
|
-
},
|
|
336
|
-
emptyContainer: {
|
|
337
|
-
padding: 32,
|
|
338
|
-
alignItems: 'center',
|
|
339
|
-
},
|
|
340
|
-
emptyText: {
|
|
341
|
-
fontSize: 16,
|
|
342
|
-
color: '#999',
|
|
343
|
-
},
|
|
344
|
-
errorText: {
|
|
345
|
-
fontSize: 16,
|
|
346
|
-
color: '#f00',
|
|
347
|
-
},
|
|
348
|
-
});
|
|
349
|
-
```
|
|
435
|
+
await waitFor(() => {
|
|
436
|
+
expect(screen.getByText('Erro de rede')).toBeTruthy();
|
|
437
|
+
});
|
|
438
|
+
});
|
|
350
439
|
|
|
351
|
-
|
|
440
|
+
it('permite tentar novamente em caso de erro', async () => {
|
|
441
|
+
renderWithNavigation();
|
|
352
442
|
|
|
353
|
-
|
|
443
|
+
await waitFor(() => {
|
|
444
|
+
expect(screen.getByText('Erro de rede')).toBeTruthy();
|
|
445
|
+
});
|
|
354
446
|
|
|
355
|
-
|
|
447
|
+
// Agora a nova tentativa será bem-sucedida
|
|
448
|
+
mockService.getById.mockResolvedValueOnce({
|
|
449
|
+
id: '123',
|
|
450
|
+
title: 'Sucesso',
|
|
451
|
+
description: 'Agora funciona',
|
|
452
|
+
createdAt: '2024-01-15',
|
|
453
|
+
});
|
|
356
454
|
|
|
357
|
-
|
|
358
|
-
import { render, screen } from '@testing-library/react-native';
|
|
359
|
-
import UserProfileScreen from './user-profile';
|
|
455
|
+
fireEvent.press(screen.getByText('Tentar novamente'));
|
|
360
456
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
457
|
+
await waitFor(() => {
|
|
458
|
+
expect(screen.getByText('Sucesso')).toBeTruthy();
|
|
459
|
+
});
|
|
460
|
+
});
|
|
365
461
|
});
|
|
366
462
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
463
|
+
describe('Acessibilidade', () => {
|
|
464
|
+
it('possui roles de acessibilidade corretas', async () => {
|
|
465
|
+
mockService.getById.mockResolvedValue({
|
|
466
|
+
id: '123',
|
|
467
|
+
title: 'Teste',
|
|
468
|
+
description: 'Desc',
|
|
469
|
+
createdAt: '2024-01-15',
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
renderWithNavigation();
|
|
473
|
+
|
|
474
|
+
await waitFor(() => {
|
|
475
|
+
expect(screen.getByRole('header', { name: 'Teste' })).toBeTruthy();
|
|
476
|
+
});
|
|
477
|
+
});
|
|
370
478
|
});
|
|
371
479
|
});
|
|
372
480
|
```
|
|
373
481
|
|
|
374
|
-
|
|
482
|
+
### Etapa 9: Variantes de Tela
|
|
375
483
|
|
|
376
|
-
|
|
484
|
+
#### Tela de Lista
|
|
377
485
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
- Rotas dinâmicas: [param].tsx
|
|
486
|
+
```tsx
|
|
487
|
+
// Para uma tela do tipo lista
|
|
488
|
+
import { FlatList } from 'react-native';
|
|
382
489
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
- ScrollView para conteúdo que pode rolar
|
|
386
|
-
- FlatList para listas longas
|
|
387
|
-
- StatusBar configurada apropriadamente
|
|
490
|
+
export function {ScreenName}ListScreen() {
|
|
491
|
+
const { data, isLoading, refresh, loadMore, hasMore } = use{ScreenName}List();
|
|
388
492
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
493
|
+
return (
|
|
494
|
+
<SafeAreaView style={styles.container}>
|
|
495
|
+
<FlatList
|
|
496
|
+
data={data}
|
|
497
|
+
keyExtractor={(item) => item.id}
|
|
498
|
+
renderItem={({ item }) => (
|
|
499
|
+
<{ScreenName}ListItem item={item} onPress={() => navigate(item.id)} />
|
|
500
|
+
)}
|
|
501
|
+
refreshControl={
|
|
502
|
+
<RefreshControl refreshing={isLoading} onRefresh={refresh} />
|
|
503
|
+
}
|
|
504
|
+
onEndReached={hasMore ? loadMore : undefined}
|
|
505
|
+
onEndReachedThreshold={0.5}
|
|
506
|
+
ListEmptyComponent={<EmptyState message="Nenhum item" />}
|
|
507
|
+
ListFooterComponent={hasMore ? <ActivityIndicator /> : null}
|
|
508
|
+
/>
|
|
509
|
+
</SafeAreaView>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
```
|
|
394
513
|
|
|
395
|
-
|
|
396
|
-
- useLocalSearchParams para parâmetros de rota
|
|
397
|
-
- router.push para navegação
|
|
398
|
-
- router.back para voltar
|
|
514
|
+
#### Tela de Formulário
|
|
399
515
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
516
|
+
```tsx
|
|
517
|
+
// Para uma tela do tipo formulário
|
|
518
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
519
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
404
520
|
|
|
405
|
-
|
|
521
|
+
export function {ScreenName}FormScreen() {
|
|
522
|
+
const { control, handleSubmit, formState } = useForm({
|
|
523
|
+
resolver: zodResolver(schema),
|
|
524
|
+
defaultValues: { name: '', email: '' },
|
|
525
|
+
});
|
|
406
526
|
|
|
407
|
-
|
|
527
|
+
const onSubmit = async (data) => {
|
|
528
|
+
// ...
|
|
529
|
+
};
|
|
408
530
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
531
|
+
return (
|
|
532
|
+
<KeyboardAvoidingView behavior="padding" style={styles.container}>
|
|
533
|
+
<ScrollView>
|
|
534
|
+
<Controller
|
|
535
|
+
control={control}
|
|
536
|
+
name="name"
|
|
537
|
+
render={({ field, fieldState }) => (
|
|
538
|
+
<TextInput
|
|
539
|
+
{...field}
|
|
540
|
+
onChangeText={field.onChange}
|
|
541
|
+
placeholder="Nome"
|
|
542
|
+
error={fieldState.error?.message}
|
|
543
|
+
/>
|
|
544
|
+
)}
|
|
545
|
+
/>
|
|
546
|
+
{/* outros campos */}
|
|
547
|
+
<Button
|
|
548
|
+
title="Salvar"
|
|
549
|
+
onPress={handleSubmit(onSubmit)}
|
|
550
|
+
loading={formState.isSubmitting}
|
|
551
|
+
/>
|
|
552
|
+
</ScrollView>
|
|
553
|
+
</KeyboardAvoidingView>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
```
|
|
417
557
|
|
|
418
|
-
|
|
558
|
+
### Resumo
|
|
419
559
|
|
|
420
|
-
|
|
560
|
+
```
|
|
561
|
+
══════════════════════════════════════════════════════════════
|
|
562
|
+
✅ TELA GERADA - {ScreenName}
|
|
563
|
+
══════════════════════════════════════════════════════════════
|
|
564
|
+
|
|
565
|
+
📁 Arquivos criados:
|
|
566
|
+
- src/screens/{ScreenName}/index.ts
|
|
567
|
+
- src/screens/{ScreenName}/{ScreenName}Screen.tsx
|
|
568
|
+
- src/screens/{ScreenName}/{ScreenName}Screen.types.ts
|
|
569
|
+
- src/screens/{ScreenName}/{ScreenName}Screen.styles.ts
|
|
570
|
+
- src/screens/{ScreenName}/{ScreenName}Screen.test.tsx
|
|
571
|
+
- src/screens/{ScreenName}/hooks/use{ScreenName}.ts
|
|
572
|
+
|
|
573
|
+
📝 Funcionalidades:
|
|
574
|
+
- TypeScript strict
|
|
575
|
+
- Navegação tipada
|
|
576
|
+
- Hook personalizado para lógica
|
|
577
|
+
- Pull-to-refresh
|
|
578
|
+
- Tratamento de estados: carregamento/erro/sucesso
|
|
579
|
+
- Acessibilidade (roles, labels)
|
|
580
|
+
- Testes completos
|
|
581
|
+
|
|
582
|
+
🔧 Próximos passos:
|
|
583
|
+
1. Adicionar a tela à navegação
|
|
584
|
+
2. Criar serviço de API
|
|
585
|
+
3. Personalizar estilos
|
|
586
|
+
4. Executar testes: npm test {ScreenName}
|
|
587
|
+
```
|