ui-ux-consultant-cli 1.0.0-beta.1
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/assets/ui-ux-consultant/SKILL.md +844 -0
- package/assets/ui-ux-consultant/references/accessibility.md +175 -0
- package/assets/ui-ux-consultant/references/alt-libraries.md +90 -0
- package/assets/ui-ux-consultant/references/animations.md +448 -0
- package/assets/ui-ux-consultant/references/catalog/colors.md +91 -0
- package/assets/ui-ux-consultant/references/catalog/fonts.md +363 -0
- package/assets/ui-ux-consultant/references/catalog/products.md +340 -0
- package/assets/ui-ux-consultant/references/catalog/styles.md +165 -0
- package/assets/ui-ux-consultant/references/components.md +1116 -0
- package/assets/ui-ux-consultant/references/patterns.md +600 -0
- package/assets/ui-ux-consultant/references/performance.md +198 -0
- package/assets/ui-ux-consultant/references/stacks/astro.md +382 -0
- package/assets/ui-ux-consultant/references/stacks/flutter.md +308 -0
- package/assets/ui-ux-consultant/references/stacks/html-tailwind.md +415 -0
- package/assets/ui-ux-consultant/references/stacks/jetpack-compose.md +333 -0
- package/assets/ui-ux-consultant/references/stacks/laravel.md +521 -0
- package/assets/ui-ux-consultant/references/stacks/nextjs.md +275 -0
- package/assets/ui-ux-consultant/references/stacks/nuxt-ui.md +384 -0
- package/assets/ui-ux-consultant/references/stacks/nuxtjs.md +264 -0
- package/assets/ui-ux-consultant/references/stacks/react-native.md +346 -0
- package/assets/ui-ux-consultant/references/stacks/react.md +268 -0
- package/assets/ui-ux-consultant/references/stacks/shadcn.md +485 -0
- package/assets/ui-ux-consultant/references/stacks/svelte.md +429 -0
- package/assets/ui-ux-consultant/references/stacks/swiftui.md +336 -0
- package/assets/ui-ux-consultant/references/stacks/threejs.md +366 -0
- package/assets/ui-ux-consultant/references/stacks/vue.md +272 -0
- package/assets/ui-ux-consultant/references/theming.md +701 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +130 -0
- package/package.json +51 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Nuxt.js UI/UX Guidelines
|
|
2
|
+
|
|
3
|
+
## When to read this
|
|
4
|
+
Read this file when building with Nuxt 3. Covers auto-imports, SSR-safe patterns, data fetching composables, server routes, state management, middleware, and performance for production Nuxt apps.
|
|
5
|
+
|
|
6
|
+
## Recommended UI Libraries
|
|
7
|
+
|
|
8
|
+
| Library | Best for | Install |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| Nuxt UI | Ready-made components (Tailwind-based) | `npx nuxi module add ui` |
|
|
11
|
+
| @pinia/nuxt | State management | `npx nuxi module add pinia` |
|
|
12
|
+
| @nuxt/image | Optimized images | `npx nuxi module add image` |
|
|
13
|
+
| @nuxt/content | Content / MDX pages | `npx nuxi module add content` |
|
|
14
|
+
| VueUse | Composable utilities (Nuxt-aware) | `npm install @vueuse/nuxt` |
|
|
15
|
+
| @nuxtjs/color-mode | Dark/light mode | `npx nuxi module add color-mode` |
|
|
16
|
+
|
|
17
|
+
## Style Recommendations by App Type
|
|
18
|
+
|
|
19
|
+
- **SaaS / product:** Nuxt UI + custom Tailwind color tokens
|
|
20
|
+
- **Marketing/content:** @nuxt/content + @nuxt/image + custom Tailwind
|
|
21
|
+
- **Dashboard:** Nuxt UI data table + sidebar layout + @pinia/nuxt
|
|
22
|
+
- **Enterprise:** Element Plus + custom Nuxt module for design tokens
|
|
23
|
+
- **E-commerce:** Custom Tailwind + @nuxt/image + Pinia cart store
|
|
24
|
+
|
|
25
|
+
## Top UX Patterns
|
|
26
|
+
|
|
27
|
+
### 1. useFetch for SSR-Aware Data Fetching
|
|
28
|
+
```typescript
|
|
29
|
+
// Preferred for simple API calls — SSR-aware, handles hydration automatically
|
|
30
|
+
const { data, pending, error, refresh } = await useFetch('/api/users');
|
|
31
|
+
|
|
32
|
+
// With options
|
|
33
|
+
const { data: posts } = await useFetch('/api/posts', {
|
|
34
|
+
query: { page: 1, limit: 10 },
|
|
35
|
+
pick: ['id', 'title', 'slug'], // Only serialize needed fields
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. useAsyncData for Complex Fetching
|
|
40
|
+
```typescript
|
|
41
|
+
// More control — custom key, transform, watch, lazy
|
|
42
|
+
const { data, pending } = await useAsyncData(
|
|
43
|
+
'users', // Cache key — must be unique per page
|
|
44
|
+
() => $fetch('/api/users'),
|
|
45
|
+
{
|
|
46
|
+
watch: [page], // Re-fetch when page changes
|
|
47
|
+
transform: data => data.users, // Transform before caching
|
|
48
|
+
lazy: true, // Don't block navigation
|
|
49
|
+
default: () => [], // Default value before fetch completes
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Server API Route
|
|
55
|
+
```typescript
|
|
56
|
+
// server/api/users.get.ts
|
|
57
|
+
export default defineEventHandler(async (event) => {
|
|
58
|
+
const query = getQuery(event);
|
|
59
|
+
const { page = 1, limit = 20 } = query;
|
|
60
|
+
|
|
61
|
+
const users = await db.user.findMany({
|
|
62
|
+
skip: (Number(page) - 1) * Number(limit),
|
|
63
|
+
take: Number(limit),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return { users, page, limit };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// server/api/users.post.ts — POST handler
|
|
70
|
+
export default defineEventHandler(async (event) => {
|
|
71
|
+
const body = await readBody(event);
|
|
72
|
+
const user = await db.user.create({ data: body });
|
|
73
|
+
return user;
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 4. SSR-Safe Shared State with useState
|
|
78
|
+
```typescript
|
|
79
|
+
// composables/useCounter.ts — shared across components, SSR-safe
|
|
80
|
+
export const useCounter = () => useState('counter', () => 0);
|
|
81
|
+
|
|
82
|
+
// Usage in any component — no hydration mismatch
|
|
83
|
+
const count = useCounter();
|
|
84
|
+
count.value++;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 5. Pinia Store with Nuxt
|
|
88
|
+
```typescript
|
|
89
|
+
// stores/cart.ts
|
|
90
|
+
export const useCartStore = defineStore('cart', () => {
|
|
91
|
+
const items = ref<CartItem[]>([]);
|
|
92
|
+
const total = computed(() => items.value.reduce((sum, i) => sum + i.price, 0));
|
|
93
|
+
|
|
94
|
+
function addItem(product: Product) {
|
|
95
|
+
const existing = items.value.find(i => i.id === product.id);
|
|
96
|
+
if (existing) existing.qty++;
|
|
97
|
+
else items.value.push({ ...product, qty: 1 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function removeItem(id: string) {
|
|
101
|
+
items.value = items.value.filter(i => i.id !== id);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { items, total, addItem, removeItem };
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 6. Route Middleware for Auth
|
|
109
|
+
```typescript
|
|
110
|
+
// middleware/auth.ts
|
|
111
|
+
export default defineNuxtRouteMiddleware((to) => {
|
|
112
|
+
const user = useUser(); // useState-based composable
|
|
113
|
+
if (!user.value) {
|
|
114
|
+
return navigateTo('/login', { redirectCode: 302 });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// pages/dashboard.vue — apply middleware
|
|
119
|
+
definePageMeta({
|
|
120
|
+
middleware: 'auth',
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 7. Page with SEO Metadata
|
|
125
|
+
```vue
|
|
126
|
+
<script setup lang="ts">
|
|
127
|
+
const route = useRoute();
|
|
128
|
+
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`);
|
|
129
|
+
|
|
130
|
+
useSeoMeta({
|
|
131
|
+
title: post.value?.title,
|
|
132
|
+
description: post.value?.excerpt,
|
|
133
|
+
ogTitle: post.value?.title,
|
|
134
|
+
ogImage: post.value?.coverImage,
|
|
135
|
+
twitterCard: 'summary_large_image',
|
|
136
|
+
});
|
|
137
|
+
</script>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 8. Client-Only Component (avoids SSR)
|
|
141
|
+
```vue
|
|
142
|
+
<!-- Wrap browser-only content -->
|
|
143
|
+
<ClientOnly>
|
|
144
|
+
<MapComponent />
|
|
145
|
+
<template #fallback>
|
|
146
|
+
<MapSkeleton />
|
|
147
|
+
</template>
|
|
148
|
+
</ClientOnly>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 9. Plugin for Global Setup
|
|
152
|
+
```typescript
|
|
153
|
+
// plugins/toast.client.ts — client-only plugin
|
|
154
|
+
export default defineNuxtPlugin(() => {
|
|
155
|
+
return {
|
|
156
|
+
provide: {
|
|
157
|
+
toast: (message: string) => {
|
|
158
|
+
// Initialize toast library here
|
|
159
|
+
window.__toast?.show(message);
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Usage in component
|
|
166
|
+
const { $toast } = useNuxtApp();
|
|
167
|
+
$toast('Saved successfully!');
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 10. Error Handling with createError
|
|
171
|
+
```typescript
|
|
172
|
+
// server/api/posts/[id].get.ts
|
|
173
|
+
export default defineEventHandler(async (event) => {
|
|
174
|
+
const id = getRouterParam(event, 'id');
|
|
175
|
+
const post = await db.post.findUnique({ where: { id } });
|
|
176
|
+
|
|
177
|
+
if (!post) {
|
|
178
|
+
throw createError({ statusCode: 404, statusMessage: 'Post not found' });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return post;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// pages/posts/[id].vue — handle errors from useFetch
|
|
185
|
+
const { data: post, error } = await useFetch(`/api/posts/${route.params.id}`);
|
|
186
|
+
if (error.value) throw createError({ fatal: true, statusCode: error.value.statusCode });
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Best Practices by Category
|
|
190
|
+
|
|
191
|
+
### Auto-Imports
|
|
192
|
+
- Components in `components/` — auto-imported with PascalCase name
|
|
193
|
+
- Composables in `composables/` — auto-imported, use `use` prefix
|
|
194
|
+
- Utilities in `utils/` — auto-imported
|
|
195
|
+
- Pinia stores in `stores/` — auto-imported with `@pinia/nuxt`
|
|
196
|
+
- Do not import Vue primitives manually (`ref`, `computed`) — auto-imported
|
|
197
|
+
|
|
198
|
+
### Data Fetching
|
|
199
|
+
- `useFetch` for simple, declarative API calls in components and pages
|
|
200
|
+
- `useAsyncData` when you need a custom cache key, transform, or `watch` deps
|
|
201
|
+
- `$fetch` only in event handlers (click, submit) — not in setup() for initial data
|
|
202
|
+
- Always provide a unique cache key to `useAsyncData` — avoid collisions between pages
|
|
203
|
+
- `lazy: true` for non-critical data that should not block navigation
|
|
204
|
+
|
|
205
|
+
### SSR Safety
|
|
206
|
+
- Never access `window`, `document`, or `localStorage` in composables or setup — wrap in `if (import.meta.client)`
|
|
207
|
+
- Use `useLocalStorage` from VueUse (`@vueuse/nuxt`) — handles SSR safely
|
|
208
|
+
- `useState` for shared state that must survive SSR hydration
|
|
209
|
+
- `<ClientOnly>` for components that only work in the browser
|
|
210
|
+
|
|
211
|
+
### State Management
|
|
212
|
+
- `useState` for simple cross-component state — SSR-safe, no extra package
|
|
213
|
+
- Pinia (`@pinia/nuxt`) for complex state with actions and getters
|
|
214
|
+
- `storeToRefs()` when destructuring Pinia stores — preserves reactivity
|
|
215
|
+
|
|
216
|
+
### Routing
|
|
217
|
+
- `definePageMeta` for page-level config: middleware, layout, keepalive, head
|
|
218
|
+
- `useRouter()` / `useRoute()` for navigation — not `$router`
|
|
219
|
+
- Named middleware files in `middleware/` — applied via `definePageMeta`
|
|
220
|
+
- `navigateTo()` for programmatic navigation — handles SSR and client
|
|
221
|
+
|
|
222
|
+
### Performance
|
|
223
|
+
- `lazy: true` on `useAsyncData` for data that does not affect initial render
|
|
224
|
+
- `@nuxt/image` on every image — automatic optimization and lazy loading
|
|
225
|
+
- `defineAsyncComponent` for heavy client-side components
|
|
226
|
+
- Server-side rendering for all public pages — avoid `ssr: false` unless absolutely needed
|
|
227
|
+
|
|
228
|
+
### Forms
|
|
229
|
+
- Server API routes as form targets — no separate API service needed
|
|
230
|
+
- Vee-Validate + Zod for schema validation
|
|
231
|
+
- `useFormData` pattern with server-side validation in API route
|
|
232
|
+
- Return structured errors from API routes: `{ error: { field: 'message' } }`
|
|
233
|
+
|
|
234
|
+
### Accessibility
|
|
235
|
+
- Use Nuxt UI components — built on Headless UI with ARIA baked in
|
|
236
|
+
- `useSeoMeta` on every public page — title, description, OG tags
|
|
237
|
+
- Consistent `<NuxtLink>` for internal navigation — renders as `<a>` with prefetch
|
|
238
|
+
- ARIA live regions for async content updates
|
|
239
|
+
|
|
240
|
+
## Common Anti-Patterns
|
|
241
|
+
|
|
242
|
+
1. `localStorage` in `setup()` — crashes SSR; use `useLocalStorage` from VueUse
|
|
243
|
+
2. `window` or `document` in composables without `if (import.meta.client)` guard
|
|
244
|
+
3. `$fetch` in component `setup()` without `useAsyncData` — no SSR, no caching, no deduplication
|
|
245
|
+
4. Manual `<head>` tags via `useHead` when `useSeoMeta` is simpler and safer
|
|
246
|
+
5. Heavy components without `<ClientOnly>` — SSR attempts to render browser-only APIs
|
|
247
|
+
6. Sequential `await useFetch` calls — use `Promise.all` with `useAsyncData`
|
|
248
|
+
7. Duplicate `useAsyncData` keys across pages — causes cache collisions and stale data
|
|
249
|
+
8. `ssr: false` on entire app — loses SEO and initial load performance
|
|
250
|
+
9. Missing `lazy: true` on non-critical data — blocks navigation unnecessarily
|
|
251
|
+
10. Not using `definePageMeta` for middleware — results in unprotected routes
|
|
252
|
+
|
|
253
|
+
## Performance Checklist
|
|
254
|
+
|
|
255
|
+
- [ ] `useAsyncData` with `lazy: true` for non-critical, below-fold data
|
|
256
|
+
- [ ] `@nuxt/image` module installed and used on all `<img>` tags
|
|
257
|
+
- [ ] Route-level `definePageMeta({ middleware: 'auth' })` for protected routes
|
|
258
|
+
- [ ] SSR data fetching via `useFetch` / `useAsyncData` — avoids client waterfalls
|
|
259
|
+
- [ ] `useState` for cross-component shared state (SSR-safe, no hydration mismatch)
|
|
260
|
+
- [ ] `<ClientOnly>` wrapping all browser-only components
|
|
261
|
+
- [ ] `useSeoMeta` on every public-facing page
|
|
262
|
+
- [ ] Server API routes return only the fields needed (use `pick` option)
|
|
263
|
+
- [ ] Pinia store persisted with `pinia-plugin-persistedstate` if needed across reloads
|
|
264
|
+
- [ ] `nitro.compressPublicAssets: true` in `nuxt.config.ts` for production
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# React Native Reference
|
|
2
|
+
|
|
3
|
+
## When to Read
|
|
4
|
+
Read this file when building React Native apps with Expo — components, styling, navigation, lists, keyboard handling, accessibility, or performance.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Recommended Libraries
|
|
9
|
+
|
|
10
|
+
| Library | Purpose | Install |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| React Navigation | Routing | `npm install @react-navigation/native` |
|
|
13
|
+
| NativeWind | Tailwind for RN | `npm install nativewind` |
|
|
14
|
+
| React Native Paper | Material components | `npm install react-native-paper` |
|
|
15
|
+
| MMKV | Fast local storage | `npm install react-native-mmkv` |
|
|
16
|
+
| Reanimated | 60fps animations (UI thread) | `npx expo install react-native-reanimated` |
|
|
17
|
+
| Zustand | Lightweight state management | `npm install zustand` |
|
|
18
|
+
| React Query / TanStack Query | Server state, caching | `npm install @tanstack/react-query` |
|
|
19
|
+
| Expo Image | Optimized image component | `npx expo install expo-image` |
|
|
20
|
+
| Zod | Schema validation | `npm install zod` |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Style Recommendations
|
|
25
|
+
|
|
26
|
+
- `StyleSheet.create` for all styles — never inline objects in JSX
|
|
27
|
+
- Follow iOS 8pt grid / Android 8dp grid for spacing
|
|
28
|
+
- Minimum touch target: 44×44pt (iOS HIG) — use `minWidth`/`minHeight` or `hitSlop`
|
|
29
|
+
- System font scales: use `fontSize` values from a scale (12, 14, 16, 18, 24, 32)
|
|
30
|
+
- NativeWind for utility-first styling in Expo projects (Tailwind classes on RN components)
|
|
31
|
+
- Avoid hardcoded colors — use a theme object or `useTheme()` from React Navigation
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Expo vs Bare Workflow
|
|
36
|
+
|
|
37
|
+
Use **Expo managed workflow** for 95% of apps:
|
|
38
|
+
- Handles native builds, OTA updates (EAS Update), and device APIs
|
|
39
|
+
- `npx expo start` for instant dev iteration
|
|
40
|
+
- EAS Build for production `.ipa`/`.apk`
|
|
41
|
+
|
|
42
|
+
Go **bare** only when you need a custom native module not in Expo SDK.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Top UX Patterns (with Code)
|
|
47
|
+
|
|
48
|
+
### StyleSheet (always — never inline objects)
|
|
49
|
+
```typescript
|
|
50
|
+
import { StyleSheet, View, Text } from 'react-native';
|
|
51
|
+
|
|
52
|
+
const styles = StyleSheet.create({
|
|
53
|
+
container: { flex: 1, backgroundColor: '#fff', padding: 16 },
|
|
54
|
+
title: { fontSize: 24, fontWeight: '700', color: '#111' },
|
|
55
|
+
card: {
|
|
56
|
+
borderRadius: 12,
|
|
57
|
+
padding: 16,
|
|
58
|
+
backgroundColor: '#f8fafc',
|
|
59
|
+
marginBottom: 12,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export function MyScreen() {
|
|
64
|
+
return (
|
|
65
|
+
<View style={styles.container}>
|
|
66
|
+
<Text style={styles.title}>Hello</Text>
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Responsive sizing
|
|
73
|
+
```typescript
|
|
74
|
+
import { useWindowDimensions } from 'react-native';
|
|
75
|
+
|
|
76
|
+
function ResponsiveLayout() {
|
|
77
|
+
const { width, height } = useWindowDimensions(); // updates on rotation
|
|
78
|
+
const isTablet = width >= 768;
|
|
79
|
+
|
|
80
|
+
return isTablet ? <TabletLayout /> : <PhoneLayout />;
|
|
81
|
+
}
|
|
82
|
+
// Never use Dimensions.get() in render — it doesn't update on rotation
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### FlatList (always for long lists)
|
|
86
|
+
```tsx
|
|
87
|
+
<FlatList
|
|
88
|
+
data={items}
|
|
89
|
+
keyExtractor={(item) => item.id}
|
|
90
|
+
renderItem={({ item }) => <ItemCard item={item} />}
|
|
91
|
+
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
|
|
92
|
+
ListEmptyComponent={<EmptyState />}
|
|
93
|
+
ListHeaderComponent={<ListHeader />}
|
|
94
|
+
onEndReached={loadMore}
|
|
95
|
+
onEndReachedThreshold={0.3}
|
|
96
|
+
refreshControl={
|
|
97
|
+
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
98
|
+
}
|
|
99
|
+
initialNumToRender={10}
|
|
100
|
+
maxToRenderPerBatch={10}
|
|
101
|
+
windowSize={5}
|
|
102
|
+
/>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Navigation (React Navigation — Stack + Tab)
|
|
106
|
+
```tsx
|
|
107
|
+
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
|
108
|
+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
109
|
+
|
|
110
|
+
const Tab = createBottomTabNavigator();
|
|
111
|
+
const Stack = createNativeStackNavigator();
|
|
112
|
+
|
|
113
|
+
function HomeStack() {
|
|
114
|
+
return (
|
|
115
|
+
<Stack.Navigator screenOptions={{ headerShown: true }}>
|
|
116
|
+
<Stack.Screen name="Feed" component={FeedScreen} />
|
|
117
|
+
<Stack.Screen name="Post" component={PostScreen} />
|
|
118
|
+
</Stack.Navigator>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function RootNavigator() {
|
|
123
|
+
return (
|
|
124
|
+
<Tab.Navigator>
|
|
125
|
+
<Tab.Screen name="Home" component={HomeStack} />
|
|
126
|
+
<Tab.Screen name="Profile" component={ProfileScreen} />
|
|
127
|
+
</Tab.Navigator>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Minimum touch target (44×44pt)
|
|
133
|
+
```tsx
|
|
134
|
+
import { TouchableOpacity, StyleSheet } from 'react-native';
|
|
135
|
+
|
|
136
|
+
<TouchableOpacity
|
|
137
|
+
style={styles.iconButton}
|
|
138
|
+
onPress={handlePress}
|
|
139
|
+
activeOpacity={0.7}
|
|
140
|
+
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
141
|
+
>
|
|
142
|
+
<Icon name="heart" size={24} />
|
|
143
|
+
</TouchableOpacity>
|
|
144
|
+
|
|
145
|
+
// styles:
|
|
146
|
+
iconButton: {
|
|
147
|
+
minWidth: 44,
|
|
148
|
+
minHeight: 44,
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
justifyContent: 'center',
|
|
151
|
+
},
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Keyboard handling
|
|
155
|
+
```tsx
|
|
156
|
+
import { KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
|
|
157
|
+
|
|
158
|
+
<KeyboardAvoidingView
|
|
159
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
160
|
+
style={{ flex: 1 }}
|
|
161
|
+
>
|
|
162
|
+
<ScrollView keyboardShouldPersistTaps="handled">
|
|
163
|
+
<TextInput
|
|
164
|
+
placeholder="Email"
|
|
165
|
+
keyboardType="email-address"
|
|
166
|
+
autoCapitalize="none"
|
|
167
|
+
returnKeyType="next"
|
|
168
|
+
/>
|
|
169
|
+
<TextInput
|
|
170
|
+
placeholder="Password"
|
|
171
|
+
secureTextEntry
|
|
172
|
+
returnKeyType="done"
|
|
173
|
+
onSubmitEditing={handleLogin}
|
|
174
|
+
/>
|
|
175
|
+
</ScrollView>
|
|
176
|
+
</KeyboardAvoidingView>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Accessible touch target
|
|
180
|
+
```tsx
|
|
181
|
+
<TouchableOpacity
|
|
182
|
+
accessible={true}
|
|
183
|
+
accessibilityLabel="Delete item"
|
|
184
|
+
accessibilityRole="button"
|
|
185
|
+
accessibilityHint="Double tap to delete this item permanently"
|
|
186
|
+
onPress={handleDelete}
|
|
187
|
+
>
|
|
188
|
+
<Icon name="trash" size={20} />
|
|
189
|
+
</TouchableOpacity>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Zustand store
|
|
193
|
+
```typescript
|
|
194
|
+
import { create } from 'zustand';
|
|
195
|
+
|
|
196
|
+
interface AuthStore {
|
|
197
|
+
user: User | null;
|
|
198
|
+
setUser: (user: User | null) => void;
|
|
199
|
+
logout: () => void;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const useAuthStore = create<AuthStore>((set) => ({
|
|
203
|
+
user: null,
|
|
204
|
+
setUser: (user) => set({ user }),
|
|
205
|
+
logout: () => set({ user: null }),
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
// In component:
|
|
209
|
+
const user = useAuthStore((state) => state.user); // subscribe to slice only
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### TanStack Query for server state
|
|
213
|
+
```tsx
|
|
214
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
215
|
+
|
|
216
|
+
function UserList() {
|
|
217
|
+
const { data, isLoading, error } = useQuery({
|
|
218
|
+
queryKey: ['users'],
|
|
219
|
+
queryFn: () => api.getUsers(),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (isLoading) return <ActivityIndicator />;
|
|
223
|
+
if (error) return <ErrorState message={error.message} />;
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<FlatList
|
|
227
|
+
data={data}
|
|
228
|
+
keyExtractor={(u) => u.id}
|
|
229
|
+
renderItem={({ item }) => <UserRow user={item} />}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Reanimated animation (runs on UI thread)
|
|
236
|
+
```tsx
|
|
237
|
+
import Animated, {
|
|
238
|
+
useSharedValue,
|
|
239
|
+
useAnimatedStyle,
|
|
240
|
+
withSpring,
|
|
241
|
+
} from 'react-native-reanimated';
|
|
242
|
+
|
|
243
|
+
function ScaleButton({ onPress }: { onPress: () => void }) {
|
|
244
|
+
const scale = useSharedValue(1);
|
|
245
|
+
|
|
246
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
247
|
+
transform: [{ scale: scale.value }],
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<Animated.View style={animatedStyle}>
|
|
252
|
+
<TouchableOpacity
|
|
253
|
+
onPressIn={() => { scale.value = withSpring(0.95); }}
|
|
254
|
+
onPressOut={() => { scale.value = withSpring(1); }}
|
|
255
|
+
onPress={onPress}
|
|
256
|
+
>
|
|
257
|
+
<Text>Press me</Text>
|
|
258
|
+
</TouchableOpacity>
|
|
259
|
+
</Animated.View>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Best Practices by Category
|
|
267
|
+
|
|
268
|
+
### Styling
|
|
269
|
+
- Always `StyleSheet.create` — objects are frozen and bridged once, not every render
|
|
270
|
+
- Avoid `style={[styles.base, condition && styles.variant]}` with long arrays — extract a helper
|
|
271
|
+
- Use `flex: 1` on container screens to fill SafeArea
|
|
272
|
+
- `Platform.select({ ios: ..., android: ... })` for platform-specific styles
|
|
273
|
+
|
|
274
|
+
### Lists
|
|
275
|
+
- `FlatList` for everything with 20+ items — never `ScrollView` + `.map`
|
|
276
|
+
- `SectionList` for grouped data
|
|
277
|
+
- `keyExtractor` must return a stable unique string — not index
|
|
278
|
+
- `getItemLayout` for known-height rows (significant performance boost)
|
|
279
|
+
- `removeClippedSubviews={true}` on Android for very long lists
|
|
280
|
+
|
|
281
|
+
### Navigation
|
|
282
|
+
- `@react-navigation/native-stack` (uses native UINavigationController) — not JS stack
|
|
283
|
+
- Pass typed params using TypeScript generic: `Stack.Screen<StackParamList, 'PostDetail'>`
|
|
284
|
+
- `useNavigation` hook inside screen components; avoid `navigation.navigate` from non-screen components
|
|
285
|
+
- Deep linking: configure `linking` prop on `NavigationContainer`
|
|
286
|
+
|
|
287
|
+
### State Management
|
|
288
|
+
- Zustand for client state — minimal boilerplate, no Provider needed
|
|
289
|
+
- TanStack Query for server state — handles caching, refetching, background updates
|
|
290
|
+
- MMKV for persisted state — 10× faster than AsyncStorage
|
|
291
|
+
- Avoid prop drilling beyond 2 levels — use Zustand slice or Context
|
|
292
|
+
|
|
293
|
+
### Keyboard & Input
|
|
294
|
+
- `KeyboardAvoidingView` wraps every screen with inputs
|
|
295
|
+
- `keyboardShouldPersistTaps="handled"` on `ScrollView` — taps dismiss keyboard only outside inputs
|
|
296
|
+
- `returnKeyType="next"` and `onSubmitEditing` to move focus between fields
|
|
297
|
+
- `textContentType` for iOS autofill; `autoComplete` for Android
|
|
298
|
+
|
|
299
|
+
### Touch & Gestures
|
|
300
|
+
- `activeOpacity={0.7}` on all `TouchableOpacity` — default 0.2 looks broken
|
|
301
|
+
- `hitSlop` for small icons — increases tap area without changing layout
|
|
302
|
+
- `Pressable` for more complex press states (hover, focus on web)
|
|
303
|
+
- Minimum 44pt touch targets everywhere — enforce with `minWidth`/`minHeight`
|
|
304
|
+
|
|
305
|
+
### Accessibility
|
|
306
|
+
- `accessibilityRole` on every interactive element
|
|
307
|
+
- `accessibilityLabel` describes what it is; `accessibilityHint` describes what happens
|
|
308
|
+
- `accessibilityState={{ disabled: true }}` for disabled controls
|
|
309
|
+
- Test with iOS VoiceOver and Android TalkBack on real devices
|
|
310
|
+
|
|
311
|
+
### Performance
|
|
312
|
+
- `React.memo` on list item components — prevents re-render when parent updates
|
|
313
|
+
- `useCallback` on `renderItem` — stable reference prevents FlatList re-renders
|
|
314
|
+
- `useMemo` for expensive transforms on large datasets
|
|
315
|
+
- Avoid anonymous functions in JSX (`onPress={() => fn(item)}`) in hot render paths — use `useCallback`
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Common Anti-Patterns
|
|
320
|
+
|
|
321
|
+
1. `ScrollView` with long lists — freezes UI thread rendering all items; use `FlatList`
|
|
322
|
+
2. Inline style objects `style={{ padding: 16 }}` — creates new object every render; use `StyleSheet.create`
|
|
323
|
+
3. Missing `keyExtractor` — React Native warns and degrades diff performance
|
|
324
|
+
4. `Dimensions.get('window')` in render — doesn't update on rotation; use `useWindowDimensions`
|
|
325
|
+
5. `activeOpacity` omitted — default `0.2` makes buttons feel unresponsive
|
|
326
|
+
6. `onPress` without `hitSlop` on small icons — nearly impossible to tap accurately
|
|
327
|
+
7. Storing server state in Zustand manually — use TanStack Query (handles stale/loading/error)
|
|
328
|
+
8. `console.log` in production — remove or use a logger that strips in release builds
|
|
329
|
+
9. No `accessibilityLabel` on icon buttons — VoiceOver announces nothing useful
|
|
330
|
+
10. Expo Go for production testing — use EAS Build; Expo Go skips native build steps
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Performance Checklist
|
|
335
|
+
|
|
336
|
+
- [ ] `FlatList` for all lists (never `ScrollView` + `.map`)
|
|
337
|
+
- [ ] `StyleSheet.create` for all styles
|
|
338
|
+
- [ ] `keyExtractor` returning stable unique string (not index)
|
|
339
|
+
- [ ] `useWindowDimensions` not `Dimensions.get` in render
|
|
340
|
+
- [ ] `React.memo` on `renderItem` components
|
|
341
|
+
- [ ] `useCallback` wrapping `renderItem` and event handlers
|
|
342
|
+
- [ ] Hermes engine enabled (default in Expo SDK 48+)
|
|
343
|
+
- [ ] `react-native-reanimated` for smooth 60fps animations (runs on UI thread)
|
|
344
|
+
- [ ] EAS Build for production — not Expo Go
|
|
345
|
+
- [ ] `getItemLayout` for fixed-height list items
|
|
346
|
+
- [ ] MMKV for persisted state (not AsyncStorage)
|