Skip to content

Commit d44d95c

Browse files
author
Alejandro Caicedo
committed
feat: add initialization guide and validation steps to init.md; enhance UI layer rules with navigation patterns and search functionality
1 parent 414bce0 commit d44d95c

7 files changed

Lines changed: 218 additions & 93 deletions

File tree

.ai/commands/init.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
description: Inicializa y valida el template
3+
agent: build
4+
---
5+
6+
Usa @TEMPLATE_USAGE.md como checklist oficial.
7+
8+
Objetivo: ejecutar automaticamente todo lo posible para dejar el proyecto listo.
9+
No inventes credenciales ni valores privados. No hagas cambios destructivos salvo que se pida explicitamente en argumentos.
10+
11+
Argumentos opcionales en $ARGUMENTS:
12+
13+
- `--clean-examples`: elimina el modulo de ejemplo y ajusta navegacion relacionada.
14+
- `--skip-checks`: no corre test/typecheck/lint.
15+
16+
Pasos a ejecutar:
17+
18+
1. Entorno
19+
20+
- Si `.env` no existe, copia `.env.example` a `.env`.
21+
- Valida que existan `API_URL`, `SERVICE_PROVIDER`, `ROOT_USERNAME`, `ROOT_PASSWORD`.
22+
- Si faltan variables, agregalas usando placeholders seguros y marcando TODO.
23+
24+
2. Firebase (solo validacion automatica)
25+
26+
- Lee `SERVICE_PROVIDER` desde `.env`.
27+
- Si `SERVICE_PROVIDER=firebase`, verifica existencia de:
28+
- `android/app/google-services.json`
29+
- `ios/GoogleService-Info.plist`
30+
- Si faltan, no crees archivos falsos: reporta accion manual requerida.
31+
32+
3. Seguridad (validacion)
33+
34+
- Verifica si `ROOT_USERNAME`/`ROOT_PASSWORD` siguen en valores por defecto.
35+
- Si siguen por defecto, reporta recomendacion para cambiarlos/removerlos en produccion.
36+
37+
4. Limpieza opcional
38+
39+
- Solo si `$ARGUMENTS` contiene `--clean-examples`, elimina:
40+
- `src/modules/examples/`
41+
- `src/navigation/stacks/ExampleStackNavigator.tsx` (si existe)
42+
- Ajusta rutas/stacks/imports para que compile sin modulo examples.
43+
- Si NO se pasa `--clean-examples`, solo reporta esta tarea como pendiente.
44+
45+
5. Verificaciones
46+
47+
- Salvo que `$ARGUMENTS` contenga `--skip-checks`, ejecuta:
48+
- `bun run test`
49+
- `bun run typecheck`
50+
- `bun run lint`
51+
52+
6. CI/CD
53+
54+
- Revisa `.github/workflows/` y reporta que cambios deben hacerse manualmente (secrets, jobs, deploy).
55+
56+
Entrega final en formato breve:
57+
58+
- `Hecho`: lista de acciones completadas.
59+
- `Pendiente manual`: lo que requiere decision/credenciales humanas.
60+
- `Errores`: comandos que fallaron y sugerencia puntual.

.ai/rules/layer-ui.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ The UI layer contains screens and screen-specific components.
88
2. **State Handling**: Use `LoadingState`, `ErrorState`, `EmptyState` from `@components/layout`
99
3. **Lists**: Use `FlashList` from `@shopify/flash-list`
1010
4. **Forms**: Use `react-hook-form` with `yupResolver`
11+
5. **Create Navigation from ListView**: There are two valid patterns to open `{Entity}FormView` from `{Entities}ListView`:
12+
- Header action icon: `<Header onPress={onAdd{Entity}} pressIcon="plus" />`
13+
- Floating Action Button: `<RootLayout fab={{ icon: 'plus', onPress: onAdd{Entity} }} />`
1114

1215
## File Structure
1316

@@ -26,18 +29,37 @@ src/modules/{module}/ui/
2629
## Golden Example: ListView Structure
2730

2831
```typescript
32+
// Variant A: FAB in RootLayout (Products)
2933
export function ProductsListView() {
30-
const [searchText, setSearchText] = useState('');
31-
const debouncedSearch = useDebounce(searchText, 500);
3234
const { navigate } = useNavigationProducts();
35+
const onAddProduct = () => navigate(ProductsRoutes.ProductForm);
36+
37+
return (
38+
<RootLayout
39+
scroll={false}
40+
toolbar={false}
41+
fab={{ icon: 'plus', onPress: onAddProduct }}
42+
>
43+
<Header title="Productos" searchbar="products" />
44+
<ProductList />
45+
</RootLayout>
46+
);
47+
}
48+
49+
// Variant B: Header action icon (Users)
50+
export function UsersListView() {
51+
const { navigate } = useNavigationUsers();
52+
const onAddUser = () => navigate(UsersRoutes.UserForm);
3353

3454
return (
3555
<RootLayout scroll={false} toolbar={false}>
3656
<Header
37-
title="Productos"
38-
onPress={() => navigate(ProductsRoutes.ProductForm)}
57+
title="Usuarios"
58+
onPress={onAddUser}
59+
pressIcon="plus"
60+
searchbar="users"
3961
/>
40-
<ProductList searchText={debouncedSearch} />
62+
<UserList />
4163
</RootLayout>
4264
);
4365
}

.ai/rules/list-view-with-filters.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ This pattern complements `layer-ui.md` and `layer-application.md`.
3535
- Guards `onEndReached` with `isConnected && hasNextPage` to avoid offline fetches
3636
- Uses `ItemSeparatorComponent` from `@components/layout`
3737
7. **ListView Screen**: `{Entities}ListView` is layout-only — it orchestrates `FiltersBar` + `List` but contains NO data-fetching logic directly. Data fetching lives inside the list component or a dedicated hook.
38+
- If the screen needs navigation to `{Entity}FormView`, use one of these valid patterns:
39+
- Header action icon: `<Header onPress={onAdd{Entity}} pressIcon="plus" ... />`
40+
- Floating Action Button: `<RootLayout fab={{ icon: 'plus', onPress: onAdd{Entity} }} ... />`
3841
8. **Persistence**: Persist active filters per source/entity via Zustand + MMKV. Re-hydrate on mount; sanitize stale keys when the source changes. Shared UI search state (e.g. for cross-screen search persistence) should live in `@modules/core/application/app.storage.ts`.
3942
9. **Query Key Fingerprint**: Serialize filters with `JSON.stringify` to build a stable cache fingerprint. Include the fingerprint in the `queryKey`.
4043

.ai/skills/components-gallery/SKILL.md

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,26 +1044,27 @@ import { RootLayout } from '@components/layout';
10441044

10451045
### Header
10461046

1047-
List screen header with title, "Agregar" button, and search input.
1047+
List screen header with title, action icon, and integrated app searchbar state.
10481048

10491049
```typescript
10501050
import { Header } from '@components/layout';
10511051
10521052
<Header
10531053
title="Productos"
10541054
onPress={() => navigate(ProductsRoutes.ProductForm)}
1055-
searchText={searchText}
1056-
setSearchText={setSearchText}
1055+
pressIcon="plus"
1056+
searchbar="products"
10571057
/>;
10581058
```
10591059

10601060
**Props:**
10611061
| Prop | Type | Description |
10621062
|---|---|---|
10631063
| `title` | `string` | Screen title (`h1` variant) |
1064-
| `onPress` | `() => void` | "Agregar" button handler |
1065-
| `searchText` | `string` | Controlled search input value |
1066-
| `setSearchText` | `(text: string) => void` | Search input handler |
1064+
| `onPress` | `() => void` | Header action handler |
1065+
| `pressIcon` | `IconName` | Optional action icon (`'menu'` by default) |
1066+
| `searchbar` | `SearchbarStorage` | Search storage key (`'products'`, `'users'`, etc.) |
1067+
| `onPressFilter` | `() => void` | Optional filter action handler |
10671068

10681069
---
10691070

@@ -1322,53 +1323,25 @@ No props.
13221323

13231324
## STANDARD LIST SCREEN PATTERN
13241325

1325-
Complete pattern combining layout components with FlashList:
1326+
Complete pattern combining layout components with FlashList (FAB navigation variant):
13261327

13271328
```typescript
1328-
import React, { useState } from 'react';
1329-
import { FlashList } from '@shopify/flash-list';
1330-
import {
1331-
RootLayout,
1332-
Header,
1333-
LoadingState,
1334-
ErrorState,
1335-
EmptyState,
1336-
ItemSeparatorComponent,
1337-
} from '@components/layout';
1338-
import { useDebounce } from '@utils/debounce';
1329+
import React from 'react';
1330+
import { RootLayout, Header } from '@components/layout';
1331+
import { ProductList } from '@modules/products/ui/components/ProductList';
13391332
13401333
export function ProductsListView() {
1341-
const [searchText, setSearchText] = useState('');
1342-
const debouncedSearch = useDebounce(searchText, 500);
13431334
const { navigate } = useNavigationProducts();
1344-
const { data, isLoading, isError, error } = useProducts({
1345-
searchText: debouncedSearch,
1346-
});
1335+
const onAddProduct = () => navigate(ProductsRoutes.ProductForm);
13471336
13481337
return (
1349-
<RootLayout scroll={false} toolbar={false}>
1350-
<Header
1351-
title="Productos"
1352-
onPress={() => navigate(ProductsRoutes.ProductForm)}
1353-
searchText={searchText}
1354-
setSearchText={setSearchText}
1355-
/>
1356-
{isLoading && <LoadingState message="Cargando productos..." />}
1357-
{isError && <ErrorState message={error?.message} onRetry={refetch} />}
1358-
{!isLoading && !isError && !data?.length && (
1359-
<EmptyState
1360-
title="Sin productos"
1361-
message="No hay productos disponibles"
1362-
/>
1363-
)}
1364-
{!isLoading && !isError && !!data?.length && (
1365-
<FlashList
1366-
data={data}
1367-
renderItem={({ item }) => <ProductItem product={item} />}
1368-
estimatedItemSize={64}
1369-
ItemSeparatorComponent={ItemSeparatorComponent}
1370-
/>
1371-
)}
1338+
<RootLayout
1339+
scroll={false}
1340+
toolbar={false}
1341+
fab={{ icon: 'plus', onPress: onAddProduct }}
1342+
>
1343+
<Header title="Productos" searchbar="products" />
1344+
<ProductList />
13721345
</RootLayout>
13731346
);
13741347
}

.ai/skills/create-layout-component/SKILL.md

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -253,63 +253,112 @@ const styles = StyleSheet.create({
253253
#### Header
254254
255255
```typescript
256-
import React from 'react';
256+
import React, { useMemo } from 'react';
257257
import { View, StyleSheet } from 'react-native';
258258
// Components
259-
import { Text, Button, TextInput } from '@components/core';
259+
import {
260+
AnimatedPressable,
261+
Icon,
262+
IconName,
263+
Text,
264+
TextInput,
265+
} from '@components/core';
260266
// Theme
261-
import { useTheme } from '@theme/index';
267+
import { spacing, useTheme } from '@theme/index';
268+
import { useAppStorage } from '@modules/core/application/app.storage';
269+
import { SearchbarStorage } from '@modules/core/application/app.storage';
270+
271+
interface SearchBarProps {
272+
searchbar: SearchbarStorage;
273+
onPressFilter?: () => void;
274+
}
275+
276+
function SearchBar({ searchbar = '', onPressFilter }: SearchBarProps) {
277+
const { searchText, setSearchText } = useAppStorage(
278+
state => state.searchbar[searchbar] || {},
279+
);
280+
281+
return (
282+
<View style={styles.searchbar}>
283+
<TextInput
284+
value={searchText}
285+
onChangeText={setSearchText}
286+
placeholder="Buscar..."
287+
containerStyle={styles.searchInput}
288+
/>
289+
{onPressFilter ? (
290+
<AnimatedPressable onPress={onPressFilter} style={styles.filterButton}>
291+
<Icon name="filter" size={spacing.base} />
292+
</AnimatedPressable>
293+
) : null}
294+
</View>
295+
);
296+
}
262297

263298
interface HeaderProps {
264299
title: string;
265300
onPress?: () => void;
266-
searchText?: string;
267-
setSearchText?: (text: string) => void;
301+
pressIcon?: IconName;
302+
searchbar?: SearchbarStorage;
303+
onPressFilter?: () => void;
268304
}
269305

270306
export function Header({
271307
title,
272308
onPress,
273-
searchText,
274-
setSearchText,
309+
pressIcon,
310+
searchbar = '',
311+
onPressFilter,
275312
}: HeaderProps) {
276-
const theme = useTheme();
313+
const {
314+
colors: { surface, border },
315+
} = useTheme();
316+
317+
const dynamicStyles = useMemo(
318+
() => [
319+
styles.container,
320+
{ backgroundColor: surface, borderBottomColor: border },
321+
],
322+
[surface, border],
323+
);
277324

278325
return (
279-
<View style={[styles.container, { borderColor: theme.colors.border }]}>
326+
<View style={dynamicStyles}>
280327
<View style={styles.row}>
281328
<Text variant="h1">{title}</Text>
282-
{onPress && (
283-
<Button variant="primary" size="sm" onPress={onPress}>
284-
Agregar
285-
</Button>
286-
)}
329+
{onPress ? (
330+
<AnimatedPressable onPress={onPress} testID="header-action-button">
331+
<Icon name={pressIcon || 'menu'} size={spacing.lg} />
332+
</AnimatedPressable>
333+
) : null}
287334
</View>
288-
{setSearchText && (
289-
<TextInput
290-
value={searchText}
291-
onChangeText={setSearchText}
292-
placeholder="Buscar..."
293-
style={styles.search}
294-
/>
295-
)}
335+
336+
<SearchBar searchbar={searchbar} onPressFilter={onPressFilter} />
296337
</View>
297338
);
298339
}
299340

300341
const styles = StyleSheet.create({
301342
container: {
302-
padding: 16,
343+
padding: spacing.md,
303344
borderBottomWidth: 1,
304-
gap: 12,
345+
gap: spacing.md,
305346
},
306347
row: {
307348
flexDirection: 'row',
308349
justifyContent: 'space-between',
309350
alignItems: 'center',
310351
},
311-
search: {
312-
marginTop: 8,
352+
searchbar: {
353+
flexDirection: 'row',
354+
alignItems: 'center',
355+
gap: spacing.sm,
356+
},
357+
searchInput: {
358+
flex: 1,
359+
},
360+
filterButton: {
361+
padding: spacing.md,
313362
},
314363
});
315364
```

0 commit comments

Comments
 (0)