@buivietphi/skill-mobile-mt 2.1.0 → 2.2.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.
@@ -0,0 +1,526 @@
1
+ # Complex UI Patterns — Production Templates
2
+
3
+ > On-demand module. Loaded when building carousels, gestures, responsive layouts, keyboard handling, or dark mode.
4
+ > Contains runnable code templates for complex UI scenarios.
5
+
6
+ ---
7
+
8
+ ## Image Carousel with Snap + Indicators
9
+
10
+ ### React Native (FlatList)
11
+
12
+ ```typescript
13
+ // components/ImageCarousel.tsx
14
+ import { FlatList, Dimensions, View, Image, StyleSheet } from 'react-native';
15
+ import { useState, useRef, useCallback } from 'react';
16
+
17
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
18
+
19
+ interface Props {
20
+ images: string[];
21
+ height?: number;
22
+ }
23
+
24
+ export function ImageCarousel({ images, height = 300 }: Props) {
25
+ const [activeIndex, setActiveIndex] = useState(0);
26
+ const flatListRef = useRef<FlatList>(null);
27
+
28
+ const onViewableItemsChanged = useCallback(({ viewableItems }: { viewableItems: Array<{ index: number | null }> }) => {
29
+ if (viewableItems[0]?.index != null) setActiveIndex(viewableItems[0].index);
30
+ }, []);
31
+
32
+ const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 50 }).current;
33
+
34
+ return (
35
+ <View>
36
+ <FlatList
37
+ ref={flatListRef}
38
+ data={images}
39
+ horizontal
40
+ pagingEnabled
41
+ showsHorizontalScrollIndicator={false}
42
+ onViewableItemsChanged={onViewableItemsChanged}
43
+ viewabilityConfig={viewabilityConfig}
44
+ keyExtractor={(_, i) => `img-${i}`}
45
+ renderItem={({ item }) => (
46
+ <Image
47
+ source={{ uri: item }}
48
+ style={{ width: SCREEN_WIDTH, height }}
49
+ resizeMode="cover"
50
+ />
51
+ )}
52
+ getItemLayout={(_, index) => ({
53
+ length: SCREEN_WIDTH,
54
+ offset: SCREEN_WIDTH * index,
55
+ index,
56
+ })}
57
+ />
58
+ {/* Indicators */}
59
+ <View style={styles.indicators}>
60
+ {images.map((_, i) => (
61
+ <View
62
+ key={i}
63
+ style={[styles.dot, i === activeIndex && styles.dotActive]}
64
+ />
65
+ ))}
66
+ </View>
67
+ </View>
68
+ );
69
+ }
70
+
71
+ const styles = StyleSheet.create({
72
+ indicators: { flexDirection: 'row', justifyContent: 'center', paddingVertical: 8 },
73
+ dot: { width: 8, height: 8, borderRadius: 4, backgroundColor: '#ccc', marginHorizontal: 4 },
74
+ dotActive: { backgroundColor: '#333', width: 24 },
75
+ });
76
+ ```
77
+
78
+ ### Flutter
79
+
80
+ ```dart
81
+ // widgets/image_carousel.dart
82
+ class ImageCarousel extends StatefulWidget {
83
+ final List<String> images;
84
+ final double height;
85
+ const ImageCarousel({required this.images, this.height = 300, super.key});
86
+
87
+ @override
88
+ State<ImageCarousel> createState() => _ImageCarouselState();
89
+ }
90
+
91
+ class _ImageCarouselState extends State<ImageCarousel> {
92
+ int _current = 0;
93
+ final _controller = PageController();
94
+
95
+ @override
96
+ Widget build(BuildContext context) {
97
+ return Column(
98
+ children: [
99
+ SizedBox(
100
+ height: widget.height,
101
+ child: PageView.builder(
102
+ controller: _controller,
103
+ onPageChanged: (i) => setState(() => _current = i),
104
+ itemCount: widget.images.length,
105
+ itemBuilder: (_, i) => Image.network(widget.images[i], fit: BoxFit.cover),
106
+ ),
107
+ ),
108
+ const SizedBox(height: 8),
109
+ Row(
110
+ mainAxisAlignment: MainAxisAlignment.center,
111
+ children: List.generate(widget.images.length, (i) => AnimatedContainer(
112
+ duration: const Duration(milliseconds: 200),
113
+ width: _current == i ? 24 : 8,
114
+ height: 8,
115
+ margin: const EdgeInsets.symmetric(horizontal: 4),
116
+ decoration: BoxDecoration(
117
+ borderRadius: BorderRadius.circular(4),
118
+ color: _current == i ? Colors.black : Colors.grey[300],
119
+ ),
120
+ )),
121
+ ),
122
+ ],
123
+ );
124
+ }
125
+ }
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Gesture Handling
131
+
132
+ ### React Native (Reanimated + Gesture Handler)
133
+
134
+ ```typescript
135
+ // components/SwipeableCard.tsx — Swipe left to delete, right to archive
136
+ import Animated, { useSharedValue, useAnimatedStyle, withSpring, runOnJS } from 'react-native-reanimated';
137
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
138
+
139
+ interface Props {
140
+ children: React.ReactNode;
141
+ onSwipeLeft?: () => void;
142
+ onSwipeRight?: () => void;
143
+ threshold?: number;
144
+ }
145
+
146
+ export function SwipeableCard({ children, onSwipeLeft, onSwipeRight, threshold = 100 }: Props) {
147
+ const translateX = useSharedValue(0);
148
+
149
+ const panGesture = Gesture.Pan()
150
+ .onUpdate((e) => {
151
+ translateX.value = e.translationX;
152
+ })
153
+ .onEnd((e) => {
154
+ if (e.translationX < -threshold && onSwipeLeft) {
155
+ runOnJS(onSwipeLeft)();
156
+ } else if (e.translationX > threshold && onSwipeRight) {
157
+ runOnJS(onSwipeRight)();
158
+ }
159
+ translateX.value = withSpring(0);
160
+ });
161
+
162
+ const animatedStyle = useAnimatedStyle(() => ({
163
+ transform: [{ translateX: translateX.value }],
164
+ }));
165
+
166
+ return (
167
+ <GestureDetector gesture={panGesture}>
168
+ <Animated.View style={animatedStyle}>
169
+ {children}
170
+ </Animated.View>
171
+ </GestureDetector>
172
+ );
173
+ }
174
+
175
+ // Long Press with Haptic
176
+ import * as Haptics from 'expo-haptics';
177
+
178
+ const longPressGesture = Gesture.LongPress()
179
+ .minDuration(500)
180
+ .onStart(() => {
181
+ runOnJS(Haptics.impactAsync)(Haptics.ImpactFeedbackStyle.Medium);
182
+ runOnJS(onLongPress)();
183
+ });
184
+ ```
185
+
186
+ ---
187
+
188
+ ## Keyboard Handling
189
+
190
+ ### React Native — Keyboard Avoidance
191
+
192
+ ```typescript
193
+ // components/KeyboardAwareView.tsx
194
+ import { KeyboardAvoidingView, Platform, TouchableWithoutFeedback, Keyboard } from 'react-native';
195
+
196
+ interface Props {
197
+ children: React.ReactNode;
198
+ }
199
+
200
+ export function KeyboardAwareView({ children }: Props) {
201
+ return (
202
+ <KeyboardAvoidingView
203
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
204
+ style={{ flex: 1 }}
205
+ keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 0} // adjust for header
206
+ >
207
+ <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
208
+ <View style={{ flex: 1 }}>
209
+ {children}
210
+ </View>
211
+ </TouchableWithoutFeedback>
212
+ </KeyboardAvoidingView>
213
+ );
214
+ }
215
+
216
+ // For ScrollView forms — use KeyboardAwareScrollView
217
+ // npm: react-native-keyboard-aware-scroll-view
218
+ import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
219
+
220
+ function FormScreen() {
221
+ return (
222
+ <KeyboardAwareScrollView
223
+ extraScrollHeight={20}
224
+ enableOnAndroid
225
+ keyboardShouldPersistTaps="handled"
226
+ >
227
+ {/* form fields */}
228
+ </KeyboardAwareScrollView>
229
+ );
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Responsive Layout
236
+
237
+ ### React Native — Tablet + Landscape Support
238
+
239
+ ```typescript
240
+ // hooks/useResponsive.ts
241
+ import { useWindowDimensions } from 'react-native';
242
+
243
+ type Breakpoint = 'phone' | 'tablet' | 'desktop';
244
+
245
+ export function useResponsive() {
246
+ const { width, height } = useWindowDimensions();
247
+
248
+ const breakpoint: Breakpoint = width >= 1024 ? 'desktop' : width >= 768 ? 'tablet' : 'phone';
249
+ const isLandscape = width > height;
250
+ const numColumns = breakpoint === 'phone' ? 1 : breakpoint === 'tablet' ? 2 : 3;
251
+
252
+ return { width, height, breakpoint, isLandscape, numColumns };
253
+ }
254
+
255
+ // Usage in list screen:
256
+ function ProductListScreen() {
257
+ const { numColumns } = useResponsive();
258
+
259
+ return (
260
+ <FlatList
261
+ data={products}
262
+ numColumns={numColumns}
263
+ key={`cols-${numColumns}`} // force re-render on column change
264
+ renderItem={({ item }) => (
265
+ <View style={{ flex: 1 / numColumns, padding: 8 }}>
266
+ <ProductCard product={item} />
267
+ </View>
268
+ )}
269
+ />
270
+ );
271
+ }
272
+ ```
273
+
274
+ ### Flutter — Responsive Layout
275
+
276
+ ```dart
277
+ // widgets/responsive_layout.dart
278
+ class ResponsiveLayout extends StatelessWidget {
279
+ final Widget phone;
280
+ final Widget? tablet;
281
+ const ResponsiveLayout({required this.phone, this.tablet, super.key});
282
+
283
+ @override
284
+ Widget build(BuildContext context) {
285
+ return LayoutBuilder(builder: (context, constraints) {
286
+ if (constraints.maxWidth >= 768 && tablet != null) return tablet!;
287
+ return phone;
288
+ });
289
+ }
290
+ }
291
+
292
+ // Usage:
293
+ ResponsiveLayout(
294
+ phone: ProductListView(columns: 1),
295
+ tablet: ProductGridView(columns: 3),
296
+ )
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Dark Mode Implementation
302
+
303
+ ### React Native — Theme System
304
+
305
+ ```typescript
306
+ // theme/ThemeProvider.tsx
307
+ import { createContext, useContext, useMemo } from 'react';
308
+ import { useColorScheme } from 'react-native';
309
+
310
+ const lightColors = {
311
+ background: '#FFFFFF',
312
+ surface: '#F5F5F5',
313
+ text: '#1A1A1A',
314
+ textSecondary: '#666666',
315
+ primary: '#007AFF',
316
+ border: '#E0E0E0',
317
+ error: '#FF3B30',
318
+ };
319
+
320
+ const darkColors = {
321
+ background: '#000000', // OLED black
322
+ surface: '#1C1C1E',
323
+ text: '#FFFFFF',
324
+ textSecondary: '#8E8E93',
325
+ primary: '#0A84FF',
326
+ border: '#38383A',
327
+ error: '#FF453A',
328
+ };
329
+
330
+ type ThemeColors = typeof lightColors;
331
+
332
+ interface Theme {
333
+ colors: ThemeColors;
334
+ isDark: boolean;
335
+ }
336
+
337
+ const ThemeContext = createContext<Theme>({} as Theme);
338
+
339
+ export function ThemeProvider({ children }: { children: React.ReactNode }) {
340
+ const colorScheme = useColorScheme();
341
+ const theme = useMemo<Theme>(() => ({
342
+ colors: colorScheme === 'dark' ? darkColors : lightColors,
343
+ isDark: colorScheme === 'dark',
344
+ }), [colorScheme]);
345
+
346
+ return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
347
+ }
348
+
349
+ export const useTheme = () => useContext(ThemeContext);
350
+
351
+ // Usage in any component:
352
+ // const { colors, isDark } = useTheme();
353
+ // <View style={{ backgroundColor: colors.background }}>
354
+ // <Text style={{ color: colors.text }}>Hello</Text>
355
+ // </View>
356
+ ```
357
+
358
+ ### Flutter — Theme Switching
359
+
360
+ ```dart
361
+ // theme/app_theme.dart
362
+ class AppTheme {
363
+ static ThemeData light = ThemeData(
364
+ brightness: Brightness.light,
365
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
366
+ useMaterial3: true,
367
+ );
368
+
369
+ static ThemeData dark = ThemeData(
370
+ brightness: Brightness.dark,
371
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark),
372
+ scaffoldBackgroundColor: Colors.black, // OLED
373
+ useMaterial3: true,
374
+ );
375
+ }
376
+
377
+ // main.dart
378
+ MaterialApp(
379
+ theme: AppTheme.light,
380
+ darkTheme: AppTheme.dark,
381
+ themeMode: ThemeMode.system, // or ThemeMode.dark / ThemeMode.light
382
+ )
383
+ ```
384
+
385
+ ---
386
+
387
+ ## Accessibility Implementation
388
+
389
+ ### React Native
390
+
391
+ ```typescript
392
+ // Accessible button with proper semantics
393
+ <TouchableOpacity
394
+ accessible
395
+ accessibilityRole="button"
396
+ accessibilityLabel="Add to cart"
397
+ accessibilityHint="Double tap to add this product to your shopping cart"
398
+ accessibilityState={{ disabled: !inStock }}
399
+ onPress={handleAddToCart}
400
+ style={[styles.button, !inStock && styles.buttonDisabled]}
401
+ >
402
+ <Text style={styles.buttonText}>{inStock ? 'Add to Cart' : 'Out of Stock'}</Text>
403
+ </TouchableOpacity>
404
+
405
+ // Image with description
406
+ <Image
407
+ source={{ uri: product.images[0] }}
408
+ accessible
409
+ accessibilityLabel={`Product image: ${product.title}`}
410
+ accessibilityRole="image"
411
+ />
412
+
413
+ // Live region for dynamic content (screen reader announces changes)
414
+ <Text accessibilityLiveRegion="polite">
415
+ {cartCount} items in cart
416
+ </Text>
417
+
418
+ // Minimum touch target: 44x44 points (iOS HIG) / 48x48 dp (Material)
419
+ const styles = StyleSheet.create({
420
+ touchTarget: {
421
+ minWidth: 44,
422
+ minHeight: 44,
423
+ justifyContent: 'center',
424
+ alignItems: 'center',
425
+ },
426
+ });
427
+ ```
428
+
429
+ ### Flutter
430
+
431
+ ```dart
432
+ // Accessible widget
433
+ Semantics(
434
+ label: 'Add to cart',
435
+ hint: 'Double tap to add this product to your shopping cart',
436
+ button: true,
437
+ enabled: inStock,
438
+ child: ElevatedButton(
439
+ onPressed: inStock ? onAddToCart : null,
440
+ child: Text(inStock ? 'Add to Cart' : 'Out of Stock'),
441
+ ),
442
+ )
443
+
444
+ // Image with semantics
445
+ Semantics(
446
+ image: true,
447
+ label: 'Product image: ${product.title}',
448
+ child: Image.network(product.images[0]),
449
+ )
450
+ ```
451
+
452
+ ### iOS SwiftUI
453
+
454
+ ```swift
455
+ Button(action: addToCart) {
456
+ Text(inStock ? "Add to Cart" : "Out of Stock")
457
+ }
458
+ .disabled(!inStock)
459
+ .accessibilityLabel("Add to cart")
460
+ .accessibilityHint("Double tap to add this product to your shopping cart")
461
+ ```
462
+
463
+ ### Android Compose
464
+
465
+ ```kotlin
466
+ Button(
467
+ onClick = { addToCart() },
468
+ enabled = inStock,
469
+ modifier = Modifier.semantics {
470
+ contentDescription = "Add to cart"
471
+ }
472
+ ) {
473
+ Text(if (inStock) "Add to Cart" else "Out of Stock")
474
+ }
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Skeleton Loading
480
+
481
+ ### React Native — Shimmer Skeleton
482
+
483
+ ```typescript
484
+ // components/Skeleton.tsx
485
+ import Animated, { useSharedValue, useAnimatedStyle, withRepeat, withTiming } from 'react-native-reanimated';
486
+ import { useEffect } from 'react';
487
+ import { useTheme } from '@/theme/ThemeProvider';
488
+
489
+ interface Props {
490
+ width: number | `${number}%`;
491
+ height: number;
492
+ borderRadius?: number;
493
+ }
494
+
495
+ export function Skeleton({ width, height, borderRadius = 4 }: Props) {
496
+ const { colors } = useTheme();
497
+ const opacity = useSharedValue(0.3);
498
+
499
+ useEffect(() => {
500
+ opacity.value = withRepeat(withTiming(1, { duration: 800 }), -1, true);
501
+ }, []);
502
+
503
+ const style = useAnimatedStyle(() => ({ opacity: opacity.value }));
504
+
505
+ return (
506
+ <Animated.View
507
+ style={[{ width, height, borderRadius, backgroundColor: colors.border }, style]}
508
+ />
509
+ );
510
+ }
511
+
512
+ // ProductDetailSkeleton.tsx
513
+ export function ProductDetailSkeleton() {
514
+ return (
515
+ <View style={{ padding: 16 }}>
516
+ <Skeleton width="100%" height={300} borderRadius={12} />
517
+ <View style={{ height: 16 }} />
518
+ <Skeleton width="70%" height={24} />
519
+ <View style={{ height: 8 }} />
520
+ <Skeleton width="30%" height={20} />
521
+ <View style={{ height: 16 }} />
522
+ <Skeleton width="100%" height={80} />
523
+ </View>
524
+ );
525
+ }
526
+ ```