@idealyst/mcp-server 1.2.18 → 1.2.20

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,2201 @@
1
+ /**
2
+ * Idealyst Recipes - Common UI Patterns
3
+ * Ready-to-use code examples for building apps with Idealyst
4
+ */
5
+ export const recipes = {
6
+ "login-form": {
7
+ name: "Login Form",
8
+ description: "A complete login form with email/password validation and error handling",
9
+ category: "auth",
10
+ difficulty: "beginner",
11
+ packages: ["@idealyst/components", "@idealyst/theme"],
12
+ code: `import React, { useState } from 'react';
13
+ import { Button, Input, Card, Text, View } from '@idealyst/components';
14
+
15
+ interface LoginFormProps {
16
+ onSubmit: (email: string, password: string) => Promise<void>;
17
+ onForgotPassword?: () => void;
18
+ }
19
+
20
+ export function LoginForm({ onSubmit, onForgotPassword }: LoginFormProps) {
21
+ const [email, setEmail] = useState('');
22
+ const [password, setPassword] = useState('');
23
+ const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
24
+ const [isLoading, setIsLoading] = useState(false);
25
+ const [submitError, setSubmitError] = useState<string | null>(null);
26
+
27
+ const validate = () => {
28
+ const newErrors: typeof errors = {};
29
+
30
+ if (!email) {
31
+ newErrors.email = 'Email is required';
32
+ } else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {
33
+ newErrors.email = 'Please enter a valid email';
34
+ }
35
+
36
+ if (!password) {
37
+ newErrors.password = 'Password is required';
38
+ } else if (password.length < 8) {
39
+ newErrors.password = 'Password must be at least 8 characters';
40
+ }
41
+
42
+ setErrors(newErrors);
43
+ return Object.keys(newErrors).length === 0;
44
+ };
45
+
46
+ const handleSubmit = async () => {
47
+ setSubmitError(null);
48
+
49
+ if (!validate()) return;
50
+
51
+ setIsLoading(true);
52
+ try {
53
+ await onSubmit(email, password);
54
+ } catch (error) {
55
+ setSubmitError(error instanceof Error ? error.message : 'Login failed');
56
+ } finally {
57
+ setIsLoading(false);
58
+ }
59
+ };
60
+
61
+ return (
62
+ <Card padding="lg">
63
+ <Text variant="headline" style={{ marginBottom: 24 }}>
64
+ Sign In
65
+ </Text>
66
+
67
+ {submitError && (
68
+ <View style={{ marginBottom: 16 }}>
69
+ <Text intent="danger">{submitError}</Text>
70
+ </View>
71
+ )}
72
+
73
+ <View style={{ gap: 16 }}>
74
+ <Input
75
+ label="Email"
76
+ placeholder="you@example.com"
77
+ value={email}
78
+ onChangeText={setEmail}
79
+ keyboardType="email-address"
80
+ autoCapitalize="none"
81
+ autoComplete="email"
82
+ error={errors.email}
83
+ />
84
+
85
+ <Input
86
+ label="Password"
87
+ placeholder="Enter your password"
88
+ value={password}
89
+ onChangeText={setPassword}
90
+ secureTextEntry
91
+ autoComplete="current-password"
92
+ error={errors.password}
93
+ />
94
+
95
+ <Button
96
+ onPress={handleSubmit}
97
+ loading={isLoading}
98
+ disabled={isLoading}
99
+ >
100
+ Sign In
101
+ </Button>
102
+
103
+ {onForgotPassword && (
104
+ <Button type="text" onPress={onForgotPassword}>
105
+ Forgot Password?
106
+ </Button>
107
+ )}
108
+ </View>
109
+ </Card>
110
+ );
111
+ }`,
112
+ explanation: `This login form demonstrates:
113
+ - Controlled inputs with useState
114
+ - Client-side validation with error messages
115
+ - Loading state during submission
116
+ - Error handling for failed login attempts
117
+ - Proper keyboard types and autocomplete hints for better UX`,
118
+ tips: [
119
+ "Add onBlur validation for immediate feedback",
120
+ "Consider using react-hook-form for complex forms",
121
+ "Store tokens securely using @idealyst/storage after successful login",
122
+ ],
123
+ relatedRecipes: ["signup-form", "forgot-password", "protected-route"],
124
+ },
125
+ "signup-form": {
126
+ name: "Signup Form",
127
+ description: "User registration form with password confirmation and terms acceptance",
128
+ category: "auth",
129
+ difficulty: "beginner",
130
+ packages: ["@idealyst/components", "@idealyst/theme"],
131
+ code: `import React, { useState } from 'react';
132
+ import { Button, Input, Card, Text, View, Checkbox, Link } from '@idealyst/components';
133
+
134
+ interface SignupFormProps {
135
+ onSubmit: (data: { name: string; email: string; password: string }) => Promise<void>;
136
+ onTermsPress?: () => void;
137
+ }
138
+
139
+ export function SignupForm({ onSubmit, onTermsPress }: SignupFormProps) {
140
+ const [name, setName] = useState('');
141
+ const [email, setEmail] = useState('');
142
+ const [password, setPassword] = useState('');
143
+ const [confirmPassword, setConfirmPassword] = useState('');
144
+ const [acceptedTerms, setAcceptedTerms] = useState(false);
145
+ const [errors, setErrors] = useState<Record<string, string>>({});
146
+ const [isLoading, setIsLoading] = useState(false);
147
+
148
+ const validate = () => {
149
+ const newErrors: Record<string, string> = {};
150
+
151
+ if (!name.trim()) {
152
+ newErrors.name = 'Name is required';
153
+ }
154
+
155
+ if (!email) {
156
+ newErrors.email = 'Email is required';
157
+ } else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {
158
+ newErrors.email = 'Please enter a valid email';
159
+ }
160
+
161
+ if (!password) {
162
+ newErrors.password = 'Password is required';
163
+ } else if (password.length < 8) {
164
+ newErrors.password = 'Password must be at least 8 characters';
165
+ }
166
+
167
+ if (password !== confirmPassword) {
168
+ newErrors.confirmPassword = 'Passwords do not match';
169
+ }
170
+
171
+ if (!acceptedTerms) {
172
+ newErrors.terms = 'You must accept the terms and conditions';
173
+ }
174
+
175
+ setErrors(newErrors);
176
+ return Object.keys(newErrors).length === 0;
177
+ };
178
+
179
+ const handleSubmit = async () => {
180
+ if (!validate()) return;
181
+
182
+ setIsLoading(true);
183
+ try {
184
+ await onSubmit({ name, email, password });
185
+ } catch (error) {
186
+ setErrors({ submit: error instanceof Error ? error.message : 'Signup failed' });
187
+ } finally {
188
+ setIsLoading(false);
189
+ }
190
+ };
191
+
192
+ return (
193
+ <Card padding="lg">
194
+ <Text variant="headline" style={{ marginBottom: 24 }}>
195
+ Create Account
196
+ </Text>
197
+
198
+ {errors.submit && (
199
+ <View style={{ marginBottom: 16 }}>
200
+ <Text intent="danger">{errors.submit}</Text>
201
+ </View>
202
+ )}
203
+
204
+ <View style={{ gap: 16 }}>
205
+ <Input
206
+ label="Full Name"
207
+ placeholder="John Doe"
208
+ value={name}
209
+ onChangeText={setName}
210
+ autoComplete="name"
211
+ error={errors.name}
212
+ />
213
+
214
+ <Input
215
+ label="Email"
216
+ placeholder="you@example.com"
217
+ value={email}
218
+ onChangeText={setEmail}
219
+ keyboardType="email-address"
220
+ autoCapitalize="none"
221
+ autoComplete="email"
222
+ error={errors.email}
223
+ />
224
+
225
+ <Input
226
+ label="Password"
227
+ placeholder="At least 8 characters"
228
+ value={password}
229
+ onChangeText={setPassword}
230
+ secureTextEntry
231
+ autoComplete="new-password"
232
+ error={errors.password}
233
+ />
234
+
235
+ <Input
236
+ label="Confirm Password"
237
+ placeholder="Confirm your password"
238
+ value={confirmPassword}
239
+ onChangeText={setConfirmPassword}
240
+ secureTextEntry
241
+ autoComplete="new-password"
242
+ error={errors.confirmPassword}
243
+ />
244
+
245
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
246
+ <Checkbox
247
+ checked={acceptedTerms}
248
+ onCheckedChange={setAcceptedTerms}
249
+ />
250
+ <Text>
251
+ I agree to the{' '}
252
+ <Link onPress={onTermsPress}>Terms and Conditions</Link>
253
+ </Text>
254
+ </View>
255
+ {errors.terms && <Text intent="danger" size="sm">{errors.terms}</Text>}
256
+
257
+ <Button
258
+ onPress={handleSubmit}
259
+ loading={isLoading}
260
+ disabled={isLoading}
261
+ >
262
+ Create Account
263
+ </Button>
264
+ </View>
265
+ </Card>
266
+ );
267
+ }`,
268
+ explanation: `This signup form includes:
269
+ - Multiple field validation including password matching
270
+ - Terms and conditions checkbox with validation
271
+ - Proper autocomplete hints for password managers
272
+ - Loading and error states`,
273
+ tips: [
274
+ "Add password strength indicator for better UX",
275
+ "Consider email verification flow after signup",
276
+ "Use secure password hashing on the backend",
277
+ ],
278
+ relatedRecipes: ["login-form", "email-verification"],
279
+ },
280
+ "settings-screen": {
281
+ name: "Settings Screen",
282
+ description: "App settings screen with toggles, selections, and grouped options",
283
+ category: "settings",
284
+ difficulty: "beginner",
285
+ packages: ["@idealyst/components", "@idealyst/theme", "@idealyst/storage"],
286
+ code: `import React, { useState, useEffect } from 'react';
287
+ import { ScrollView } from 'react-native';
288
+ import {
289
+ View, Text, Switch, Select, Card, Divider, Icon
290
+ } from '@idealyst/components';
291
+ import { storage } from '@idealyst/storage';
292
+
293
+ interface Settings {
294
+ notifications: boolean;
295
+ emailUpdates: boolean;
296
+ darkMode: boolean;
297
+ language: string;
298
+ fontSize: string;
299
+ }
300
+
301
+ const defaultSettings: Settings = {
302
+ notifications: true,
303
+ emailUpdates: false,
304
+ darkMode: false,
305
+ language: 'en',
306
+ fontSize: 'medium',
307
+ };
308
+
309
+ export function SettingsScreen() {
310
+ const [settings, setSettings] = useState<Settings>(defaultSettings);
311
+ const [isLoading, setIsLoading] = useState(true);
312
+
313
+ useEffect(() => {
314
+ loadSettings();
315
+ }, []);
316
+
317
+ const loadSettings = async () => {
318
+ try {
319
+ const saved = await storage.get<Settings>('user-settings');
320
+ if (saved) {
321
+ setSettings(saved);
322
+ }
323
+ } finally {
324
+ setIsLoading(false);
325
+ }
326
+ };
327
+
328
+ const updateSetting = async <K extends keyof Settings>(
329
+ key: K,
330
+ value: Settings[K]
331
+ ) => {
332
+ const newSettings = { ...settings, [key]: value };
333
+ setSettings(newSettings);
334
+ await storage.set('user-settings', newSettings);
335
+ };
336
+
337
+ if (isLoading) {
338
+ return <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
339
+ <Text>Loading...</Text>
340
+ </View>;
341
+ }
342
+
343
+ return (
344
+ <ScrollView style={{ flex: 1 }}>
345
+ <View style={{ padding: 16, gap: 16 }}>
346
+ {/* Notifications Section */}
347
+ <Card>
348
+ <Text variant="title" style={{ marginBottom: 16 }}>
349
+ Notifications
350
+ </Text>
351
+
352
+ <SettingRow
353
+ icon="bell"
354
+ label="Push Notifications"
355
+ description="Receive push notifications"
356
+ >
357
+ <Switch
358
+ checked={settings.notifications}
359
+ onCheckedChange={(v) => updateSetting('notifications', v)}
360
+ />
361
+ </SettingRow>
362
+
363
+ <Divider />
364
+
365
+ <SettingRow
366
+ icon="email"
367
+ label="Email Updates"
368
+ description="Receive weekly email updates"
369
+ >
370
+ <Switch
371
+ checked={settings.emailUpdates}
372
+ onCheckedChange={(v) => updateSetting('emailUpdates', v)}
373
+ />
374
+ </SettingRow>
375
+ </Card>
376
+
377
+ {/* Appearance Section */}
378
+ <Card>
379
+ <Text variant="title" style={{ marginBottom: 16 }}>
380
+ Appearance
381
+ </Text>
382
+
383
+ <SettingRow
384
+ icon="theme-light-dark"
385
+ label="Dark Mode"
386
+ description="Use dark theme"
387
+ >
388
+ <Switch
389
+ checked={settings.darkMode}
390
+ onCheckedChange={(v) => updateSetting('darkMode', v)}
391
+ />
392
+ </SettingRow>
393
+
394
+ <Divider />
395
+
396
+ <SettingRow
397
+ icon="format-size"
398
+ label="Font Size"
399
+ >
400
+ <Select
401
+ value={settings.fontSize}
402
+ onValueChange={(v) => updateSetting('fontSize', v)}
403
+ options={[
404
+ { label: 'Small', value: 'small' },
405
+ { label: 'Medium', value: 'medium' },
406
+ { label: 'Large', value: 'large' },
407
+ ]}
408
+ style={{ width: 120 }}
409
+ />
410
+ </SettingRow>
411
+ </Card>
412
+
413
+ {/* Language Section */}
414
+ <Card>
415
+ <Text variant="title" style={{ marginBottom: 16 }}>
416
+ Language & Region
417
+ </Text>
418
+
419
+ <SettingRow
420
+ icon="translate"
421
+ label="Language"
422
+ >
423
+ <Select
424
+ value={settings.language}
425
+ onValueChange={(v) => updateSetting('language', v)}
426
+ options={[
427
+ { label: 'English', value: 'en' },
428
+ { label: 'Spanish', value: 'es' },
429
+ { label: 'French', value: 'fr' },
430
+ { label: 'German', value: 'de' },
431
+ ]}
432
+ style={{ width: 140 }}
433
+ />
434
+ </SettingRow>
435
+ </Card>
436
+ </View>
437
+ </ScrollView>
438
+ );
439
+ }
440
+
441
+ // Helper component for consistent setting rows
442
+ function SettingRow({
443
+ icon,
444
+ label,
445
+ description,
446
+ children
447
+ }: {
448
+ icon: string;
449
+ label: string;
450
+ description?: string;
451
+ children: React.ReactNode;
452
+ }) {
453
+ return (
454
+ <View style={{
455
+ flexDirection: 'row',
456
+ alignItems: 'center',
457
+ justifyContent: 'space-between',
458
+ paddingVertical: 12,
459
+ }}>
460
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, flex: 1 }}>
461
+ <Icon name={icon} size={24} />
462
+ <View style={{ flex: 1 }}>
463
+ <Text>{label}</Text>
464
+ {description && (
465
+ <Text size="sm" style={{ opacity: 0.7 }}>{description}</Text>
466
+ )}
467
+ </View>
468
+ </View>
469
+ {children}
470
+ </View>
471
+ );
472
+ }`,
473
+ explanation: `This settings screen demonstrates:
474
+ - Loading and persisting settings with @idealyst/storage
475
+ - Grouped settings sections with Cards
476
+ - Switch toggles for boolean options
477
+ - Select dropdowns for choices
478
+ - Reusable SettingRow component for consistent layout`,
479
+ tips: [
480
+ "Consider debouncing saves for rapid toggles",
481
+ "Add a 'Reset to Defaults' option",
482
+ "Sync settings with backend for cross-device consistency",
483
+ ],
484
+ relatedRecipes: ["theme-switcher", "profile-screen"],
485
+ },
486
+ "theme-switcher": {
487
+ name: "Theme Switcher",
488
+ description: "Toggle between light and dark mode with persistence",
489
+ category: "settings",
490
+ difficulty: "beginner",
491
+ packages: ["@idealyst/components", "@idealyst/theme", "@idealyst/storage"],
492
+ code: `import React, { createContext, useContext, useEffect, useState } from 'react';
493
+ import { UnistylesRuntime } from 'react-native-unistyles';
494
+ import { storage } from '@idealyst/storage';
495
+ import { Switch, View, Text, Icon } from '@idealyst/components';
496
+
497
+ type ThemeMode = 'light' | 'dark' | 'system';
498
+
499
+ interface ThemeContextType {
500
+ mode: ThemeMode;
501
+ setMode: (mode: ThemeMode) => void;
502
+ isDark: boolean;
503
+ }
504
+
505
+ const ThemeContext = createContext<ThemeContextType | null>(null);
506
+
507
+ export function ThemeProvider({ children }: { children: React.ReactNode }) {
508
+ const [mode, setModeState] = useState<ThemeMode>('system');
509
+ const [isLoaded, setIsLoaded] = useState(false);
510
+
511
+ useEffect(() => {
512
+ loadTheme();
513
+ }, []);
514
+
515
+ useEffect(() => {
516
+ if (!isLoaded) return;
517
+
518
+ // Apply theme based on mode
519
+ if (mode === 'system') {
520
+ UnistylesRuntime.setAdaptiveThemes(true);
521
+ } else {
522
+ UnistylesRuntime.setAdaptiveThemes(false);
523
+ UnistylesRuntime.setTheme(mode);
524
+ }
525
+ }, [mode, isLoaded]);
526
+
527
+ const loadTheme = async () => {
528
+ const saved = await storage.get<ThemeMode>('theme-mode');
529
+ if (saved) {
530
+ setModeState(saved);
531
+ }
532
+ setIsLoaded(true);
533
+ };
534
+
535
+ const setMode = async (newMode: ThemeMode) => {
536
+ setModeState(newMode);
537
+ await storage.set('theme-mode', newMode);
538
+ };
539
+
540
+ const isDark = mode === 'dark' ||
541
+ (mode === 'system' && UnistylesRuntime.colorScheme === 'dark');
542
+
543
+ if (!isLoaded) return null;
544
+
545
+ return (
546
+ <ThemeContext.Provider value={{ mode, setMode, isDark }}>
547
+ {children}
548
+ </ThemeContext.Provider>
549
+ );
550
+ }
551
+
552
+ export function useTheme() {
553
+ const context = useContext(ThemeContext);
554
+ if (!context) {
555
+ throw new Error('useTheme must be used within ThemeProvider');
556
+ }
557
+ return context;
558
+ }
559
+
560
+ // Simple toggle component
561
+ export function ThemeToggle() {
562
+ const { isDark, setMode } = useTheme();
563
+
564
+ return (
565
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
566
+ <Icon name={isDark ? 'weather-night' : 'weather-sunny'} size={24} />
567
+ <Text>Dark Mode</Text>
568
+ <Switch
569
+ checked={isDark}
570
+ onCheckedChange={(checked) => setMode(checked ? 'dark' : 'light')}
571
+ />
572
+ </View>
573
+ );
574
+ }
575
+
576
+ // Full selector with system option
577
+ export function ThemeSelector() {
578
+ const { mode, setMode } = useTheme();
579
+
580
+ return (
581
+ <View style={{ gap: 8 }}>
582
+ <ThemeOption
583
+ label="Light"
584
+ icon="weather-sunny"
585
+ selected={mode === 'light'}
586
+ onPress={() => setMode('light')}
587
+ />
588
+ <ThemeOption
589
+ label="Dark"
590
+ icon="weather-night"
591
+ selected={mode === 'dark'}
592
+ onPress={() => setMode('dark')}
593
+ />
594
+ <ThemeOption
595
+ label="System"
596
+ icon="cellphone"
597
+ selected={mode === 'system'}
598
+ onPress={() => setMode('system')}
599
+ />
600
+ </View>
601
+ );
602
+ }
603
+
604
+ function ThemeOption({
605
+ label,
606
+ icon,
607
+ selected,
608
+ onPress
609
+ }: {
610
+ label: string;
611
+ icon: string;
612
+ selected: boolean;
613
+ onPress: () => void;
614
+ }) {
615
+ return (
616
+ <Pressable onPress={onPress}>
617
+ <View style={{
618
+ flexDirection: 'row',
619
+ alignItems: 'center',
620
+ gap: 12,
621
+ padding: 12,
622
+ borderRadius: 8,
623
+ backgroundColor: selected ? 'rgba(0,0,0,0.1)' : 'transparent',
624
+ }}>
625
+ <Icon name={icon} size={20} />
626
+ <Text>{label}</Text>
627
+ {selected && <Icon name="check" size={20} intent="success" />}
628
+ </View>
629
+ </Pressable>
630
+ );
631
+ }`,
632
+ explanation: `This theme switcher provides:
633
+ - ThemeProvider context for app-wide theme state
634
+ - Persistence with @idealyst/storage
635
+ - Support for light, dark, and system-follow modes
636
+ - Integration with Unistyles runtime
637
+ - Both simple toggle and full selector UI components`,
638
+ tips: [
639
+ "Wrap your app root with ThemeProvider",
640
+ "The system option follows device settings automatically",
641
+ "Theme changes are instant with no reload required",
642
+ ],
643
+ relatedRecipes: ["settings-screen"],
644
+ },
645
+ "tab-navigation": {
646
+ name: "Tab Navigation",
647
+ description: "Bottom tab navigation with icons and badges",
648
+ category: "navigation",
649
+ difficulty: "beginner",
650
+ packages: ["@idealyst/components", "@idealyst/navigation"],
651
+ code: `import React from 'react';
652
+ import { Router, TabBar } from '@idealyst/navigation';
653
+ import { Icon, Badge, View } from '@idealyst/components';
654
+
655
+ // Define your screens
656
+ function HomeScreen() {
657
+ return <View><Text>Home</Text></View>;
658
+ }
659
+
660
+ function SearchScreen() {
661
+ return <View><Text>Search</Text></View>;
662
+ }
663
+
664
+ function NotificationsScreen() {
665
+ return <View><Text>Notifications</Text></View>;
666
+ }
667
+
668
+ function ProfileScreen() {
669
+ return <View><Text>Profile</Text></View>;
670
+ }
671
+
672
+ // Route configuration
673
+ const routes = {
674
+ home: {
675
+ path: '/',
676
+ screen: HomeScreen,
677
+ options: {
678
+ title: 'Home',
679
+ tabBarIcon: ({ focused }: { focused: boolean }) => (
680
+ <Icon name={focused ? 'home' : 'home-outline'} size={24} />
681
+ ),
682
+ },
683
+ },
684
+ search: {
685
+ path: '/search',
686
+ screen: SearchScreen,
687
+ options: {
688
+ title: 'Search',
689
+ tabBarIcon: ({ focused }: { focused: boolean }) => (
690
+ <Icon name={focused ? 'magnify' : 'magnify'} size={24} />
691
+ ),
692
+ },
693
+ },
694
+ notifications: {
695
+ path: '/notifications',
696
+ screen: NotificationsScreen,
697
+ options: {
698
+ title: 'Notifications',
699
+ tabBarIcon: ({ focused }: { focused: boolean }) => (
700
+ <View>
701
+ <Icon name={focused ? 'bell' : 'bell-outline'} size={24} />
702
+ {/* Show badge when there are unread notifications */}
703
+ <Badge
704
+ count={3}
705
+ style={{ position: 'absolute', top: -4, right: -8 }}
706
+ />
707
+ </View>
708
+ ),
709
+ },
710
+ },
711
+ profile: {
712
+ path: '/profile',
713
+ screen: ProfileScreen,
714
+ options: {
715
+ title: 'Profile',
716
+ tabBarIcon: ({ focused }: { focused: boolean }) => (
717
+ <Icon name={focused ? 'account' : 'account-outline'} size={24} />
718
+ ),
719
+ },
720
+ },
721
+ };
722
+
723
+ export function App() {
724
+ return (
725
+ <Router
726
+ routes={routes}
727
+ navigator="tabs"
728
+ tabBarPosition="bottom"
729
+ />
730
+ );
731
+ }`,
732
+ explanation: `This tab navigation setup includes:
733
+ - Four tabs with icons that change when focused
734
+ - Badge on notifications tab for unread count
735
+ - Type-safe route configuration
736
+ - Works on both web and native`,
737
+ tips: [
738
+ "Use outline/filled icon variants to indicate focus state",
739
+ "Keep tab count to 3-5 for best usability",
740
+ "Consider hiding tabs on certain screens (like detail views)",
741
+ ],
742
+ relatedRecipes: ["drawer-navigation", "stack-navigation", "protected-route"],
743
+ },
744
+ "drawer-navigation": {
745
+ name: "Drawer Navigation",
746
+ description: "Side drawer menu with navigation items and user profile",
747
+ category: "navigation",
748
+ difficulty: "intermediate",
749
+ packages: ["@idealyst/components", "@idealyst/navigation"],
750
+ code: `import React from 'react';
751
+ import { Router, useNavigator } from '@idealyst/navigation';
752
+ import { View, Text, Icon, Avatar, Pressable, Divider } from '@idealyst/components';
753
+
754
+ // Custom drawer content
755
+ function DrawerContent() {
756
+ const { navigate, currentRoute } = useNavigator();
757
+
758
+ const menuItems = [
759
+ { route: 'home', icon: 'home', label: 'Home' },
760
+ { route: 'dashboard', icon: 'view-dashboard', label: 'Dashboard' },
761
+ { route: 'messages', icon: 'message', label: 'Messages' },
762
+ { route: 'settings', icon: 'cog', label: 'Settings' },
763
+ ];
764
+
765
+ return (
766
+ <View style={{ flex: 1, padding: 16 }}>
767
+ {/* User Profile Header */}
768
+ <View style={{ alignItems: 'center', paddingVertical: 24 }}>
769
+ <Avatar
770
+ source={{ uri: 'https://example.com/avatar.jpg' }}
771
+ size="lg"
772
+ />
773
+ <Text variant="title" style={{ marginTop: 12 }}>John Doe</Text>
774
+ <Text size="sm" style={{ opacity: 0.7 }}>john@example.com</Text>
775
+ </View>
776
+
777
+ <Divider style={{ marginVertical: 16 }} />
778
+
779
+ {/* Menu Items */}
780
+ <View style={{ gap: 4 }}>
781
+ {menuItems.map((item) => (
782
+ <DrawerItem
783
+ key={item.route}
784
+ icon={item.icon}
785
+ label={item.label}
786
+ active={currentRoute === item.route}
787
+ onPress={() => navigate(item.route)}
788
+ />
789
+ ))}
790
+ </View>
791
+
792
+ {/* Footer */}
793
+ <View style={{ marginTop: 'auto' }}>
794
+ <Divider style={{ marginVertical: 16 }} />
795
+ <DrawerItem
796
+ icon="logout"
797
+ label="Sign Out"
798
+ onPress={() => {
799
+ // Handle logout
800
+ }}
801
+ />
802
+ </View>
803
+ </View>
804
+ );
805
+ }
806
+
807
+ function DrawerItem({
808
+ icon,
809
+ label,
810
+ active,
811
+ onPress
812
+ }: {
813
+ icon: string;
814
+ label: string;
815
+ active?: boolean;
816
+ onPress: () => void;
817
+ }) {
818
+ return (
819
+ <Pressable onPress={onPress}>
820
+ <View style={{
821
+ flexDirection: 'row',
822
+ alignItems: 'center',
823
+ gap: 16,
824
+ padding: 12,
825
+ borderRadius: 8,
826
+ backgroundColor: active ? 'rgba(0,0,0,0.1)' : 'transparent',
827
+ }}>
828
+ <Icon name={icon} size={24} intent={active ? 'primary' : undefined} />
829
+ <Text intent={active ? 'primary' : undefined}>{label}</Text>
830
+ </View>
831
+ </Pressable>
832
+ );
833
+ }
834
+
835
+ // Route configuration
836
+ const routes = {
837
+ home: { path: '/', screen: HomeScreen },
838
+ dashboard: { path: '/dashboard', screen: DashboardScreen },
839
+ messages: { path: '/messages', screen: MessagesScreen },
840
+ settings: { path: '/settings', screen: SettingsScreen },
841
+ };
842
+
843
+ export function App() {
844
+ return (
845
+ <Router
846
+ routes={routes}
847
+ navigator="drawer"
848
+ drawerContent={DrawerContent}
849
+ />
850
+ );
851
+ }`,
852
+ explanation: `This drawer navigation includes:
853
+ - Custom drawer content with user profile
854
+ - Active state highlighting for current route
855
+ - Grouped menu items with icons
856
+ - Sign out button at the bottom
857
+ - Works on both web (sidebar) and native (slide-out drawer)`,
858
+ tips: [
859
+ "Add a hamburger menu button to open drawer on native",
860
+ "Consider using drawer on tablet/desktop, tabs on mobile",
861
+ "Add gesture support for swipe-to-open on native",
862
+ ],
863
+ relatedRecipes: ["tab-navigation", "stack-navigation"],
864
+ },
865
+ "protected-route": {
866
+ name: "Protected Routes",
867
+ description: "Redirect unauthenticated users to login with auth state management",
868
+ category: "auth",
869
+ difficulty: "intermediate",
870
+ packages: ["@idealyst/navigation", "@idealyst/storage", "@idealyst/components"],
871
+ code: `import React, { createContext, useContext, useEffect, useState } from 'react';
872
+ import { Router, useNavigator } from '@idealyst/navigation';
873
+ import { storage } from '@idealyst/storage';
874
+ import { View, Text, ActivityIndicator } from '@idealyst/components';
875
+
876
+ // Auth Context
877
+ interface User {
878
+ id: string;
879
+ email: string;
880
+ name: string;
881
+ }
882
+
883
+ interface AuthContextType {
884
+ user: User | null;
885
+ isLoading: boolean;
886
+ login: (email: string, password: string) => Promise<void>;
887
+ logout: () => Promise<void>;
888
+ }
889
+
890
+ const AuthContext = createContext<AuthContextType | null>(null);
891
+
892
+ export function AuthProvider({ children }: { children: React.ReactNode }) {
893
+ const [user, setUser] = useState<User | null>(null);
894
+ const [isLoading, setIsLoading] = useState(true);
895
+
896
+ useEffect(() => {
897
+ checkAuth();
898
+ }, []);
899
+
900
+ const checkAuth = async () => {
901
+ try {
902
+ const token = await storage.get<string>('auth-token');
903
+ if (token) {
904
+ // Validate token and get user data
905
+ const userData = await fetchUser(token);
906
+ setUser(userData);
907
+ }
908
+ } catch (error) {
909
+ // Token invalid or expired
910
+ await storage.remove('auth-token');
911
+ } finally {
912
+ setIsLoading(false);
913
+ }
914
+ };
915
+
916
+ const login = async (email: string, password: string) => {
917
+ const { token, user } = await apiLogin(email, password);
918
+ await storage.set('auth-token', token);
919
+ setUser(user);
920
+ };
921
+
922
+ const logout = async () => {
923
+ await storage.remove('auth-token');
924
+ setUser(null);
925
+ };
926
+
927
+ return (
928
+ <AuthContext.Provider value={{ user, isLoading, login, logout }}>
929
+ {children}
930
+ </AuthContext.Provider>
931
+ );
932
+ }
933
+
934
+ export function useAuth() {
935
+ const context = useContext(AuthContext);
936
+ if (!context) {
937
+ throw new Error('useAuth must be used within AuthProvider');
938
+ }
939
+ return context;
940
+ }
941
+
942
+ // Protected Route Wrapper
943
+ function ProtectedRoute({ children }: { children: React.ReactNode }) {
944
+ const { user, isLoading } = useAuth();
945
+ const { navigate } = useNavigator();
946
+
947
+ useEffect(() => {
948
+ if (!isLoading && !user) {
949
+ navigate('login');
950
+ }
951
+ }, [user, isLoading]);
952
+
953
+ if (isLoading) {
954
+ return (
955
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
956
+ <ActivityIndicator size="lg" />
957
+ </View>
958
+ );
959
+ }
960
+
961
+ if (!user) {
962
+ return null; // Will redirect
963
+ }
964
+
965
+ return <>{children}</>;
966
+ }
967
+
968
+ // Route configuration
969
+ const routes = {
970
+ login: {
971
+ path: '/login',
972
+ screen: LoginScreen,
973
+ options: { public: true },
974
+ },
975
+ signup: {
976
+ path: '/signup',
977
+ screen: SignupScreen,
978
+ options: { public: true },
979
+ },
980
+ home: {
981
+ path: '/',
982
+ screen: () => (
983
+ <ProtectedRoute>
984
+ <HomeScreen />
985
+ </ProtectedRoute>
986
+ ),
987
+ },
988
+ profile: {
989
+ path: '/profile',
990
+ screen: () => (
991
+ <ProtectedRoute>
992
+ <ProfileScreen />
993
+ </ProtectedRoute>
994
+ ),
995
+ },
996
+ settings: {
997
+ path: '/settings',
998
+ screen: () => (
999
+ <ProtectedRoute>
1000
+ <SettingsScreen />
1001
+ </ProtectedRoute>
1002
+ ),
1003
+ },
1004
+ };
1005
+
1006
+ export function App() {
1007
+ return (
1008
+ <AuthProvider>
1009
+ <Router routes={routes} />
1010
+ </AuthProvider>
1011
+ );
1012
+ }`,
1013
+ explanation: `This protected routes setup includes:
1014
+ - AuthProvider context for app-wide auth state
1015
+ - Token persistence with @idealyst/storage
1016
+ - Loading state while checking authentication
1017
+ - Automatic redirect to login for unauthenticated users
1018
+ - ProtectedRoute wrapper component for easy use`,
1019
+ tips: [
1020
+ "Add token refresh logic for long-lived sessions",
1021
+ "Consider deep link handling for login redirects",
1022
+ "Use @idealyst/oauth-client for OAuth flows",
1023
+ ],
1024
+ relatedRecipes: ["login-form", "oauth-flow"],
1025
+ },
1026
+ "data-list": {
1027
+ name: "Data List with Pull-to-Refresh",
1028
+ description: "Scrollable list with pull-to-refresh, loading states, and empty state",
1029
+ category: "data",
1030
+ difficulty: "intermediate",
1031
+ packages: ["@idealyst/components"],
1032
+ code: `import React, { useState, useEffect, useCallback } from 'react';
1033
+ import { FlatList, RefreshControl } from 'react-native';
1034
+ import { View, Text, Card, ActivityIndicator, Button, Icon } from '@idealyst/components';
1035
+
1036
+ interface Item {
1037
+ id: string;
1038
+ title: string;
1039
+ description: string;
1040
+ createdAt: string;
1041
+ }
1042
+
1043
+ interface DataListProps {
1044
+ fetchItems: () => Promise<Item[]>;
1045
+ onItemPress?: (item: Item) => void;
1046
+ }
1047
+
1048
+ export function DataList({ fetchItems, onItemPress }: DataListProps) {
1049
+ const [items, setItems] = useState<Item[]>([]);
1050
+ const [isLoading, setIsLoading] = useState(true);
1051
+ const [isRefreshing, setIsRefreshing] = useState(false);
1052
+ const [error, setError] = useState<string | null>(null);
1053
+
1054
+ const loadData = useCallback(async (showLoader = true) => {
1055
+ if (showLoader) setIsLoading(true);
1056
+ setError(null);
1057
+
1058
+ try {
1059
+ const data = await fetchItems();
1060
+ setItems(data);
1061
+ } catch (err) {
1062
+ setError(err instanceof Error ? err.message : 'Failed to load data');
1063
+ } finally {
1064
+ setIsLoading(false);
1065
+ setIsRefreshing(false);
1066
+ }
1067
+ }, [fetchItems]);
1068
+
1069
+ useEffect(() => {
1070
+ loadData();
1071
+ }, [loadData]);
1072
+
1073
+ const handleRefresh = () => {
1074
+ setIsRefreshing(true);
1075
+ loadData(false);
1076
+ };
1077
+
1078
+ // Loading state
1079
+ if (isLoading) {
1080
+ return (
1081
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
1082
+ <ActivityIndicator size="lg" />
1083
+ <Text style={{ marginTop: 16 }}>Loading...</Text>
1084
+ </View>
1085
+ );
1086
+ }
1087
+
1088
+ // Error state
1089
+ if (error) {
1090
+ return (
1091
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 }}>
1092
+ <Icon name="alert-circle" size={48} intent="danger" />
1093
+ <Text variant="title" style={{ marginTop: 16 }}>Something went wrong</Text>
1094
+ <Text style={{ textAlign: 'center', marginTop: 8, opacity: 0.7 }}>
1095
+ {error}
1096
+ </Text>
1097
+ <Button onPress={() => loadData()} style={{ marginTop: 24 }}>
1098
+ Try Again
1099
+ </Button>
1100
+ </View>
1101
+ );
1102
+ }
1103
+
1104
+ // Empty state
1105
+ if (items.length === 0) {
1106
+ return (
1107
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 }}>
1108
+ <Icon name="inbox" size={48} style={{ opacity: 0.5 }} />
1109
+ <Text variant="title" style={{ marginTop: 16 }}>No items yet</Text>
1110
+ <Text style={{ textAlign: 'center', marginTop: 8, opacity: 0.7 }}>
1111
+ Pull down to refresh or check back later
1112
+ </Text>
1113
+ </View>
1114
+ );
1115
+ }
1116
+
1117
+ return (
1118
+ <FlatList
1119
+ data={items}
1120
+ keyExtractor={(item) => item.id}
1121
+ contentContainerStyle={{ padding: 16, gap: 12 }}
1122
+ refreshControl={
1123
+ <RefreshControl
1124
+ refreshing={isRefreshing}
1125
+ onRefresh={handleRefresh}
1126
+ />
1127
+ }
1128
+ renderItem={({ item }) => (
1129
+ <Card
1130
+ onPress={() => onItemPress?.(item)}
1131
+ style={{ padding: 16 }}
1132
+ >
1133
+ <Text variant="title">{item.title}</Text>
1134
+ <Text style={{ marginTop: 4, opacity: 0.7 }}>
1135
+ {item.description}
1136
+ </Text>
1137
+ <Text size="sm" style={{ marginTop: 8, opacity: 0.5 }}>
1138
+ {new Date(item.createdAt).toLocaleDateString()}
1139
+ </Text>
1140
+ </Card>
1141
+ )}
1142
+ />
1143
+ );
1144
+ }
1145
+
1146
+ // Usage example
1147
+ function MyScreen() {
1148
+ const fetchItems = async () => {
1149
+ const response = await fetch('/api/items');
1150
+ return response.json();
1151
+ };
1152
+
1153
+ return (
1154
+ <DataList
1155
+ fetchItems={fetchItems}
1156
+ onItemPress={(item) => console.log('Selected:', item)}
1157
+ />
1158
+ );
1159
+ }`,
1160
+ explanation: `This data list component handles:
1161
+ - Initial loading state with spinner
1162
+ - Pull-to-refresh functionality
1163
+ - Error state with retry button
1164
+ - Empty state with helpful message
1165
+ - Efficient FlatList rendering for large lists`,
1166
+ tips: [
1167
+ "Add pagination with onEndReached for large datasets",
1168
+ "Use skeleton loading for smoother perceived performance",
1169
+ "Consider optimistic updates for better UX",
1170
+ ],
1171
+ relatedRecipes: ["search-filter", "infinite-scroll"],
1172
+ },
1173
+ "search-filter": {
1174
+ name: "Search with Filters",
1175
+ description: "Search input with filter chips and debounced search",
1176
+ category: "data",
1177
+ difficulty: "intermediate",
1178
+ packages: ["@idealyst/components"],
1179
+ code: `import React, { useState, useEffect, useMemo } from 'react';
1180
+ import { ScrollView } from 'react-native';
1181
+ import { View, Input, Chip, Text, Icon } from '@idealyst/components';
1182
+
1183
+ interface SearchFilterProps<T> {
1184
+ data: T[];
1185
+ searchKeys: (keyof T)[];
1186
+ filterOptions: { key: string; label: string; values: string[] }[];
1187
+ renderItem: (item: T) => React.ReactNode;
1188
+ placeholder?: string;
1189
+ }
1190
+
1191
+ // Debounce hook
1192
+ function useDebounce<T>(value: T, delay: number): T {
1193
+ const [debouncedValue, setDebouncedValue] = useState(value);
1194
+
1195
+ useEffect(() => {
1196
+ const timer = setTimeout(() => setDebouncedValue(value), delay);
1197
+ return () => clearTimeout(timer);
1198
+ }, [value, delay]);
1199
+
1200
+ return debouncedValue;
1201
+ }
1202
+
1203
+ export function SearchFilter<T extends Record<string, any>>({
1204
+ data,
1205
+ searchKeys,
1206
+ filterOptions,
1207
+ renderItem,
1208
+ placeholder = 'Search...',
1209
+ }: SearchFilterProps<T>) {
1210
+ const [searchQuery, setSearchQuery] = useState('');
1211
+ const [activeFilters, setActiveFilters] = useState<Record<string, string[]>>({});
1212
+
1213
+ const debouncedQuery = useDebounce(searchQuery, 300);
1214
+
1215
+ const toggleFilter = (key: string, value: string) => {
1216
+ setActiveFilters((prev) => {
1217
+ const current = prev[key] || [];
1218
+ const updated = current.includes(value)
1219
+ ? current.filter((v) => v !== value)
1220
+ : [...current, value];
1221
+
1222
+ return { ...prev, [key]: updated };
1223
+ });
1224
+ };
1225
+
1226
+ const clearFilters = () => {
1227
+ setActiveFilters({});
1228
+ setSearchQuery('');
1229
+ };
1230
+
1231
+ const filteredData = useMemo(() => {
1232
+ let result = data;
1233
+
1234
+ // Apply search
1235
+ if (debouncedQuery) {
1236
+ const query = debouncedQuery.toLowerCase();
1237
+ result = result.filter((item) =>
1238
+ searchKeys.some((key) =>
1239
+ String(item[key]).toLowerCase().includes(query)
1240
+ )
1241
+ );
1242
+ }
1243
+
1244
+ // Apply filters
1245
+ for (const [key, values] of Object.entries(activeFilters)) {
1246
+ if (values.length > 0) {
1247
+ result = result.filter((item) => values.includes(String(item[key])));
1248
+ }
1249
+ }
1250
+
1251
+ return result;
1252
+ }, [data, debouncedQuery, activeFilters, searchKeys]);
1253
+
1254
+ const hasActiveFilters =
1255
+ searchQuery || Object.values(activeFilters).some((v) => v.length > 0);
1256
+
1257
+ return (
1258
+ <View style={{ flex: 1 }}>
1259
+ {/* Search Input */}
1260
+ <View style={{ padding: 16 }}>
1261
+ <Input
1262
+ placeholder={placeholder}
1263
+ value={searchQuery}
1264
+ onChangeText={setSearchQuery}
1265
+ leftIcon="magnify"
1266
+ rightIcon={searchQuery ? 'close' : undefined}
1267
+ onRightIconPress={() => setSearchQuery('')}
1268
+ />
1269
+ </View>
1270
+
1271
+ {/* Filter Chips */}
1272
+ {filterOptions.map((filter) => (
1273
+ <View key={filter.key} style={{ paddingHorizontal: 16, marginBottom: 12 }}>
1274
+ <Text size="sm" style={{ marginBottom: 8, opacity: 0.7 }}>
1275
+ {filter.label}
1276
+ </Text>
1277
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
1278
+ <View style={{ flexDirection: 'row', gap: 8 }}>
1279
+ {filter.values.map((value) => (
1280
+ <Chip
1281
+ key={value}
1282
+ selected={(activeFilters[filter.key] || []).includes(value)}
1283
+ onPress={() => toggleFilter(filter.key, value)}
1284
+ >
1285
+ {value}
1286
+ </Chip>
1287
+ ))}
1288
+ </View>
1289
+ </ScrollView>
1290
+ </View>
1291
+ ))}
1292
+
1293
+ {/* Results Header */}
1294
+ <View style={{
1295
+ flexDirection: 'row',
1296
+ justifyContent: 'space-between',
1297
+ alignItems: 'center',
1298
+ paddingHorizontal: 16,
1299
+ paddingVertical: 8,
1300
+ }}>
1301
+ <Text size="sm" style={{ opacity: 0.7 }}>
1302
+ {filteredData.length} result{filteredData.length !== 1 ? 's' : ''}
1303
+ </Text>
1304
+ {hasActiveFilters && (
1305
+ <Chip onPress={clearFilters} size="sm">
1306
+ <Icon name="close" size={14} /> Clear all
1307
+ </Chip>
1308
+ )}
1309
+ </View>
1310
+
1311
+ {/* Results */}
1312
+ <ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, gap: 12 }}>
1313
+ {filteredData.length === 0 ? (
1314
+ <View style={{ alignItems: 'center', paddingVertical: 32 }}>
1315
+ <Icon name="magnify-close" size={48} style={{ opacity: 0.5 }} />
1316
+ <Text style={{ marginTop: 16 }}>No results found</Text>
1317
+ </View>
1318
+ ) : (
1319
+ filteredData.map((item, index) => (
1320
+ <View key={index}>{renderItem(item)}</View>
1321
+ ))
1322
+ )}
1323
+ </ScrollView>
1324
+ </View>
1325
+ );
1326
+ }
1327
+
1328
+ // Usage example
1329
+ const products = [
1330
+ { id: '1', name: 'iPhone', category: 'Electronics', price: 999 },
1331
+ { id: '2', name: 'MacBook', category: 'Electronics', price: 1999 },
1332
+ { id: '3', name: 'Desk Chair', category: 'Furniture', price: 299 },
1333
+ ];
1334
+
1335
+ function ProductSearch() {
1336
+ return (
1337
+ <SearchFilter
1338
+ data={products}
1339
+ searchKeys={['name']}
1340
+ filterOptions={[
1341
+ { key: 'category', label: 'Category', values: ['Electronics', 'Furniture'] },
1342
+ ]}
1343
+ renderItem={(product) => (
1344
+ <Card>
1345
+ <Text>{product.name}</Text>
1346
+ <Text>\${product.price}</Text>
1347
+ </Card>
1348
+ )}
1349
+ />
1350
+ );
1351
+ }`,
1352
+ explanation: `This search and filter component provides:
1353
+ - Debounced search input (300ms delay)
1354
+ - Multiple filter categories with chips
1355
+ - Combined search + filter logic
1356
+ - Clear all filters button
1357
+ - Result count display
1358
+ - Empty state handling`,
1359
+ tips: [
1360
+ "Add URL query params sync for shareable filtered views",
1361
+ "Consider server-side filtering for large datasets",
1362
+ "Add sort options alongside filters",
1363
+ ],
1364
+ relatedRecipes: ["data-list", "infinite-scroll"],
1365
+ },
1366
+ "modal-confirmation": {
1367
+ name: "Confirmation Dialog",
1368
+ description: "Reusable confirmation modal for destructive actions",
1369
+ category: "layout",
1370
+ difficulty: "beginner",
1371
+ packages: ["@idealyst/components"],
1372
+ code: `import React, { createContext, useContext, useState, useCallback } from 'react';
1373
+ import { Dialog, Button, Text, View, Icon } from '@idealyst/components';
1374
+
1375
+ interface ConfirmOptions {
1376
+ title: string;
1377
+ message: string;
1378
+ confirmLabel?: string;
1379
+ cancelLabel?: string;
1380
+ intent?: 'danger' | 'warning' | 'primary';
1381
+ icon?: string;
1382
+ }
1383
+
1384
+ interface ConfirmContextType {
1385
+ confirm: (options: ConfirmOptions) => Promise<boolean>;
1386
+ }
1387
+
1388
+ const ConfirmContext = createContext<ConfirmContextType | null>(null);
1389
+
1390
+ export function ConfirmProvider({ children }: { children: React.ReactNode }) {
1391
+ const [isOpen, setIsOpen] = useState(false);
1392
+ const [options, setOptions] = useState<ConfirmOptions | null>(null);
1393
+ const [resolveRef, setResolveRef] = useState<((value: boolean) => void) | null>(null);
1394
+
1395
+ const confirm = useCallback((opts: ConfirmOptions): Promise<boolean> => {
1396
+ return new Promise((resolve) => {
1397
+ setOptions(opts);
1398
+ setResolveRef(() => resolve);
1399
+ setIsOpen(true);
1400
+ });
1401
+ }, []);
1402
+
1403
+ const handleClose = (confirmed: boolean) => {
1404
+ setIsOpen(false);
1405
+ resolveRef?.(confirmed);
1406
+ // Clean up after animation
1407
+ setTimeout(() => {
1408
+ setOptions(null);
1409
+ setResolveRef(null);
1410
+ }, 300);
1411
+ };
1412
+
1413
+ return (
1414
+ <ConfirmContext.Provider value={{ confirm }}>
1415
+ {children}
1416
+
1417
+ <Dialog open={isOpen} onOpenChange={(open) => !open && handleClose(false)}>
1418
+ {options && (
1419
+ <View style={{ padding: 24, alignItems: 'center' }}>
1420
+ {options.icon && (
1421
+ <Icon
1422
+ name={options.icon}
1423
+ size={48}
1424
+ intent={options.intent || 'danger'}
1425
+ style={{ marginBottom: 16 }}
1426
+ />
1427
+ )}
1428
+
1429
+ <Text variant="headline" style={{ textAlign: 'center' }}>
1430
+ {options.title}
1431
+ </Text>
1432
+
1433
+ <Text style={{ textAlign: 'center', marginTop: 8, opacity: 0.7 }}>
1434
+ {options.message}
1435
+ </Text>
1436
+
1437
+ <View style={{
1438
+ flexDirection: 'row',
1439
+ gap: 12,
1440
+ marginTop: 24,
1441
+ width: '100%',
1442
+ }}>
1443
+ <Button
1444
+ type="outlined"
1445
+ onPress={() => handleClose(false)}
1446
+ style={{ flex: 1 }}
1447
+ >
1448
+ {options.cancelLabel || 'Cancel'}
1449
+ </Button>
1450
+
1451
+ <Button
1452
+ intent={options.intent || 'danger'}
1453
+ onPress={() => handleClose(true)}
1454
+ style={{ flex: 1 }}
1455
+ >
1456
+ {options.confirmLabel || 'Confirm'}
1457
+ </Button>
1458
+ </View>
1459
+ </View>
1460
+ )}
1461
+ </Dialog>
1462
+ </ConfirmContext.Provider>
1463
+ );
1464
+ }
1465
+
1466
+ export function useConfirm() {
1467
+ const context = useContext(ConfirmContext);
1468
+ if (!context) {
1469
+ throw new Error('useConfirm must be used within ConfirmProvider');
1470
+ }
1471
+ return context.confirm;
1472
+ }
1473
+
1474
+ // Usage example
1475
+ function DeleteButton({ onDelete }: { onDelete: () => void }) {
1476
+ const confirm = useConfirm();
1477
+
1478
+ const handleDelete = async () => {
1479
+ const confirmed = await confirm({
1480
+ title: 'Delete Item?',
1481
+ message: 'This action cannot be undone. Are you sure you want to delete this item?',
1482
+ confirmLabel: 'Delete',
1483
+ cancelLabel: 'Keep',
1484
+ intent: 'danger',
1485
+ icon: 'delete',
1486
+ });
1487
+
1488
+ if (confirmed) {
1489
+ onDelete();
1490
+ }
1491
+ };
1492
+
1493
+ return (
1494
+ <Button intent="danger" type="outlined" onPress={handleDelete}>
1495
+ Delete
1496
+ </Button>
1497
+ );
1498
+ }
1499
+
1500
+ // Wrap your app
1501
+ function App() {
1502
+ return (
1503
+ <ConfirmProvider>
1504
+ <MyApp />
1505
+ </ConfirmProvider>
1506
+ );
1507
+ }`,
1508
+ explanation: `This confirmation dialog system provides:
1509
+ - Async/await API for easy use: \`if (await confirm({...})) { ... }\`
1510
+ - Customizable title, message, buttons, and icon
1511
+ - Intent-based styling (danger, warning, primary)
1512
+ - Promise-based resolution
1513
+ - Clean context-based architecture`,
1514
+ tips: [
1515
+ "Use danger intent for destructive actions",
1516
+ "Keep messages concise and actionable",
1517
+ "Consider adding a 'Don't ask again' checkbox for repeated actions",
1518
+ ],
1519
+ relatedRecipes: ["toast-notifications"],
1520
+ },
1521
+ "toast-notifications": {
1522
+ name: "Toast Notifications",
1523
+ description: "Temporary notification messages that auto-dismiss",
1524
+ category: "layout",
1525
+ difficulty: "intermediate",
1526
+ packages: ["@idealyst/components"],
1527
+ code: `import React, { createContext, useContext, useState, useCallback } from 'react';
1528
+ import { Animated, Pressable } from 'react-native';
1529
+ import { View, Text, Icon } from '@idealyst/components';
1530
+
1531
+ type ToastType = 'success' | 'error' | 'warning' | 'info';
1532
+
1533
+ interface Toast {
1534
+ id: string;
1535
+ type: ToastType;
1536
+ message: string;
1537
+ duration?: number;
1538
+ }
1539
+
1540
+ interface ToastContextType {
1541
+ showToast: (type: ToastType, message: string, duration?: number) => void;
1542
+ success: (message: string) => void;
1543
+ error: (message: string) => void;
1544
+ warning: (message: string) => void;
1545
+ info: (message: string) => void;
1546
+ }
1547
+
1548
+ const ToastContext = createContext<ToastContextType | null>(null);
1549
+
1550
+ const toastConfig: Record<ToastType, { icon: string; intent: string }> = {
1551
+ success: { icon: 'check-circle', intent: 'success' },
1552
+ error: { icon: 'alert-circle', intent: 'danger' },
1553
+ warning: { icon: 'alert', intent: 'warning' },
1554
+ info: { icon: 'information', intent: 'primary' },
1555
+ };
1556
+
1557
+ export function ToastProvider({ children }: { children: React.ReactNode }) {
1558
+ const [toasts, setToasts] = useState<Toast[]>([]);
1559
+
1560
+ const removeToast = useCallback((id: string) => {
1561
+ setToasts((prev) => prev.filter((t) => t.id !== id));
1562
+ }, []);
1563
+
1564
+ const showToast = useCallback((type: ToastType, message: string, duration = 3000) => {
1565
+ const id = Date.now().toString();
1566
+ const toast: Toast = { id, type, message, duration };
1567
+
1568
+ setToasts((prev) => [...prev, toast]);
1569
+
1570
+ if (duration > 0) {
1571
+ setTimeout(() => removeToast(id), duration);
1572
+ }
1573
+ }, [removeToast]);
1574
+
1575
+ const contextValue: ToastContextType = {
1576
+ showToast,
1577
+ success: (msg) => showToast('success', msg),
1578
+ error: (msg) => showToast('error', msg),
1579
+ warning: (msg) => showToast('warning', msg),
1580
+ info: (msg) => showToast('info', msg),
1581
+ };
1582
+
1583
+ return (
1584
+ <ToastContext.Provider value={contextValue}>
1585
+ {children}
1586
+
1587
+ {/* Toast Container */}
1588
+ <View
1589
+ style={{
1590
+ position: 'absolute',
1591
+ top: 60,
1592
+ left: 16,
1593
+ right: 16,
1594
+ zIndex: 9999,
1595
+ gap: 8,
1596
+ }}
1597
+ pointerEvents="box-none"
1598
+ >
1599
+ {toasts.map((toast) => (
1600
+ <ToastItem
1601
+ key={toast.id}
1602
+ toast={toast}
1603
+ onDismiss={() => removeToast(toast.id)}
1604
+ />
1605
+ ))}
1606
+ </View>
1607
+ </ToastContext.Provider>
1608
+ );
1609
+ }
1610
+
1611
+ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }) {
1612
+ const fadeAnim = React.useRef(new Animated.Value(0)).current;
1613
+ const config = toastConfig[toast.type];
1614
+
1615
+ React.useEffect(() => {
1616
+ Animated.timing(fadeAnim, {
1617
+ toValue: 1,
1618
+ duration: 200,
1619
+ useNativeDriver: true,
1620
+ }).start();
1621
+ }, []);
1622
+
1623
+ return (
1624
+ <Animated.View style={{ opacity: fadeAnim, transform: [{ translateY: fadeAnim.interpolate({
1625
+ inputRange: [0, 1],
1626
+ outputRange: [-20, 0],
1627
+ }) }] }}>
1628
+ <Pressable onPress={onDismiss}>
1629
+ <View
1630
+ style={{
1631
+ flexDirection: 'row',
1632
+ alignItems: 'center',
1633
+ gap: 12,
1634
+ padding: 16,
1635
+ borderRadius: 8,
1636
+ backgroundColor: '#1a1a1a',
1637
+ shadowColor: '#000',
1638
+ shadowOffset: { width: 0, height: 2 },
1639
+ shadowOpacity: 0.25,
1640
+ shadowRadius: 4,
1641
+ elevation: 5,
1642
+ }}
1643
+ >
1644
+ <Icon name={config.icon} size={20} intent={config.intent as any} />
1645
+ <Text style={{ flex: 1, color: '#fff' }}>{toast.message}</Text>
1646
+ <Icon name="close" size={16} style={{ opacity: 0.5 }} />
1647
+ </View>
1648
+ </Pressable>
1649
+ </Animated.View>
1650
+ );
1651
+ }
1652
+
1653
+ export function useToast() {
1654
+ const context = useContext(ToastContext);
1655
+ if (!context) {
1656
+ throw new Error('useToast must be used within ToastProvider');
1657
+ }
1658
+ return context;
1659
+ }
1660
+
1661
+ // Usage example
1662
+ function SaveButton() {
1663
+ const toast = useToast();
1664
+ const [isSaving, setIsSaving] = useState(false);
1665
+
1666
+ const handleSave = async () => {
1667
+ setIsSaving(true);
1668
+ try {
1669
+ await saveData();
1670
+ toast.success('Changes saved successfully!');
1671
+ } catch (error) {
1672
+ toast.error('Failed to save changes. Please try again.');
1673
+ } finally {
1674
+ setIsSaving(false);
1675
+ }
1676
+ };
1677
+
1678
+ return (
1679
+ <Button onPress={handleSave} loading={isSaving}>
1680
+ Save
1681
+ </Button>
1682
+ );
1683
+ }
1684
+
1685
+ // Wrap your app
1686
+ function App() {
1687
+ return (
1688
+ <ToastProvider>
1689
+ <MyApp />
1690
+ </ToastProvider>
1691
+ );
1692
+ }`,
1693
+ explanation: `This toast notification system provides:
1694
+ - Simple API: \`toast.success('Message')\`
1695
+ - Four types: success, error, warning, info
1696
+ - Auto-dismiss with configurable duration
1697
+ - Tap to dismiss
1698
+ - Animated entrance
1699
+ - Stacking multiple toasts`,
1700
+ tips: [
1701
+ "Use success for completed actions, error for failures",
1702
+ "Keep messages under 50 characters for readability",
1703
+ "Don't show toasts for every action - use sparingly",
1704
+ ],
1705
+ relatedRecipes: ["modal-confirmation"],
1706
+ },
1707
+ "form-with-validation": {
1708
+ name: "Form with Validation",
1709
+ description: "Multi-field form with real-time validation and error handling",
1710
+ category: "forms",
1711
+ difficulty: "intermediate",
1712
+ packages: ["@idealyst/components"],
1713
+ code: `import React, { useState } from 'react';
1714
+ import { ScrollView } from 'react-native';
1715
+ import {
1716
+ View, Text, Input, Select, Checkbox, Button, Card
1717
+ } from '@idealyst/components';
1718
+
1719
+ // Validation rules
1720
+ type ValidationRule<T> = {
1721
+ validate: (value: T, formData: FormData) => boolean;
1722
+ message: string;
1723
+ };
1724
+
1725
+ interface FormData {
1726
+ name: string;
1727
+ email: string;
1728
+ phone: string;
1729
+ country: string;
1730
+ message: string;
1731
+ subscribe: boolean;
1732
+ }
1733
+
1734
+ const validationRules: Partial<Record<keyof FormData, ValidationRule<any>[]>> = {
1735
+ name: [
1736
+ { validate: (v) => v.trim().length > 0, message: 'Name is required' },
1737
+ { validate: (v) => v.trim().length >= 2, message: 'Name must be at least 2 characters' },
1738
+ ],
1739
+ email: [
1740
+ { validate: (v) => v.length > 0, message: 'Email is required' },
1741
+ { validate: (v) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v), message: 'Invalid email format' },
1742
+ ],
1743
+ phone: [
1744
+ { validate: (v) => !v || /^[+]?[0-9\\s-]{10,}$/.test(v), message: 'Invalid phone number' },
1745
+ ],
1746
+ country: [
1747
+ { validate: (v) => v.length > 0, message: 'Please select a country' },
1748
+ ],
1749
+ message: [
1750
+ { validate: (v) => v.length > 0, message: 'Message is required' },
1751
+ { validate: (v) => v.length >= 10, message: 'Message must be at least 10 characters' },
1752
+ ],
1753
+ };
1754
+
1755
+ export function ContactForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {
1756
+ const [formData, setFormData] = useState<FormData>({
1757
+ name: '',
1758
+ email: '',
1759
+ phone: '',
1760
+ country: '',
1761
+ message: '',
1762
+ subscribe: false,
1763
+ });
1764
+
1765
+ const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
1766
+ const [touched, setTouched] = useState<Partial<Record<keyof FormData, boolean>>>({});
1767
+ const [isSubmitting, setIsSubmitting] = useState(false);
1768
+
1769
+ const validateField = (field: keyof FormData, value: any): string | undefined => {
1770
+ const rules = validationRules[field];
1771
+ if (!rules) return undefined;
1772
+
1773
+ for (const rule of rules) {
1774
+ if (!rule.validate(value, formData)) {
1775
+ return rule.message;
1776
+ }
1777
+ }
1778
+ return undefined;
1779
+ };
1780
+
1781
+ const validateAll = (): boolean => {
1782
+ const newErrors: typeof errors = {};
1783
+ let isValid = true;
1784
+
1785
+ for (const field of Object.keys(validationRules) as (keyof FormData)[]) {
1786
+ const error = validateField(field, formData[field]);
1787
+ if (error) {
1788
+ newErrors[field] = error;
1789
+ isValid = false;
1790
+ }
1791
+ }
1792
+
1793
+ setErrors(newErrors);
1794
+ return isValid;
1795
+ };
1796
+
1797
+ const handleChange = (field: keyof FormData, value: any) => {
1798
+ setFormData((prev) => ({ ...prev, [field]: value }));
1799
+
1800
+ // Validate on change if field was touched
1801
+ if (touched[field]) {
1802
+ const error = validateField(field, value);
1803
+ setErrors((prev) => ({ ...prev, [field]: error }));
1804
+ }
1805
+ };
1806
+
1807
+ const handleBlur = (field: keyof FormData) => {
1808
+ setTouched((prev) => ({ ...prev, [field]: true }));
1809
+ const error = validateField(field, formData[field]);
1810
+ setErrors((prev) => ({ ...prev, [field]: error }));
1811
+ };
1812
+
1813
+ const handleSubmit = async () => {
1814
+ // Mark all fields as touched
1815
+ const allTouched = Object.keys(formData).reduce(
1816
+ (acc, key) => ({ ...acc, [key]: true }),
1817
+ {}
1818
+ );
1819
+ setTouched(allTouched);
1820
+
1821
+ if (!validateAll()) return;
1822
+
1823
+ setIsSubmitting(true);
1824
+ try {
1825
+ await onSubmit(formData);
1826
+ } catch (error) {
1827
+ setErrors({
1828
+ submit: error instanceof Error ? error.message : 'Submission failed'
1829
+ } as any);
1830
+ } finally {
1831
+ setIsSubmitting(false);
1832
+ }
1833
+ };
1834
+
1835
+ return (
1836
+ <ScrollView>
1837
+ <Card padding="lg">
1838
+ <Text variant="headline" style={{ marginBottom: 24 }}>
1839
+ Contact Us
1840
+ </Text>
1841
+
1842
+ <View style={{ gap: 16 }}>
1843
+ <Input
1844
+ label="Name *"
1845
+ placeholder="Your full name"
1846
+ value={formData.name}
1847
+ onChangeText={(v) => handleChange('name', v)}
1848
+ onBlur={() => handleBlur('name')}
1849
+ error={touched.name ? errors.name : undefined}
1850
+ />
1851
+
1852
+ <Input
1853
+ label="Email *"
1854
+ placeholder="you@example.com"
1855
+ value={formData.email}
1856
+ onChangeText={(v) => handleChange('email', v)}
1857
+ onBlur={() => handleBlur('email')}
1858
+ keyboardType="email-address"
1859
+ autoCapitalize="none"
1860
+ error={touched.email ? errors.email : undefined}
1861
+ />
1862
+
1863
+ <Input
1864
+ label="Phone"
1865
+ placeholder="+1 234 567 8900"
1866
+ value={formData.phone}
1867
+ onChangeText={(v) => handleChange('phone', v)}
1868
+ onBlur={() => handleBlur('phone')}
1869
+ keyboardType="phone-pad"
1870
+ error={touched.phone ? errors.phone : undefined}
1871
+ />
1872
+
1873
+ <Select
1874
+ label="Country *"
1875
+ placeholder="Select your country"
1876
+ value={formData.country}
1877
+ onValueChange={(v) => handleChange('country', v)}
1878
+ options={[
1879
+ { label: 'United States', value: 'us' },
1880
+ { label: 'United Kingdom', value: 'uk' },
1881
+ { label: 'Canada', value: 'ca' },
1882
+ { label: 'Australia', value: 'au' },
1883
+ { label: 'Other', value: 'other' },
1884
+ ]}
1885
+ error={touched.country ? errors.country : undefined}
1886
+ />
1887
+
1888
+ <Input
1889
+ label="Message *"
1890
+ placeholder="How can we help you?"
1891
+ value={formData.message}
1892
+ onChangeText={(v) => handleChange('message', v)}
1893
+ onBlur={() => handleBlur('message')}
1894
+ multiline
1895
+ numberOfLines={4}
1896
+ error={touched.message ? errors.message : undefined}
1897
+ />
1898
+
1899
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
1900
+ <Checkbox
1901
+ checked={formData.subscribe}
1902
+ onCheckedChange={(v) => handleChange('subscribe', v)}
1903
+ />
1904
+ <Text>Subscribe to our newsletter</Text>
1905
+ </View>
1906
+
1907
+ <Button
1908
+ onPress={handleSubmit}
1909
+ loading={isSubmitting}
1910
+ disabled={isSubmitting}
1911
+ style={{ marginTop: 8 }}
1912
+ >
1913
+ Send Message
1914
+ </Button>
1915
+ </View>
1916
+ </Card>
1917
+ </ScrollView>
1918
+ );
1919
+ }`,
1920
+ explanation: `This form demonstrates:
1921
+ - Field-level validation with custom rules
1922
+ - Validation on blur (after first touch)
1923
+ - Real-time validation after field is touched
1924
+ - Full form validation on submit
1925
+ - Error display with touched state tracking
1926
+ - Loading state during submission`,
1927
+ tips: [
1928
+ "Consider using a form library like react-hook-form for complex forms",
1929
+ "Add success state/message after submission",
1930
+ "Implement autosave for long forms",
1931
+ ],
1932
+ relatedRecipes: ["login-form", "signup-form"],
1933
+ },
1934
+ "image-upload": {
1935
+ name: "Image Upload",
1936
+ description: "Image picker with preview, crop option, and upload progress",
1937
+ category: "media",
1938
+ difficulty: "intermediate",
1939
+ packages: ["@idealyst/components", "@idealyst/camera"],
1940
+ code: `import React, { useState } from 'react';
1941
+ import { Image } from 'react-native';
1942
+ import { View, Text, Button, Card, Icon, Progress } from '@idealyst/components';
1943
+ // Note: You'll need expo-image-picker or react-native-image-picker
1944
+
1945
+ interface ImageUploadProps {
1946
+ onUpload: (uri: string) => Promise<string>; // Returns uploaded URL
1947
+ currentImage?: string;
1948
+ }
1949
+
1950
+ export function ImageUpload({ onUpload, currentImage }: ImageUploadProps) {
1951
+ const [imageUri, setImageUri] = useState<string | null>(currentImage || null);
1952
+ const [isUploading, setIsUploading] = useState(false);
1953
+ const [uploadProgress, setUploadProgress] = useState(0);
1954
+ const [error, setError] = useState<string | null>(null);
1955
+
1956
+ const pickImage = async () => {
1957
+ try {
1958
+ // Using expo-image-picker as example
1959
+ const result = await ImagePicker.launchImageLibraryAsync({
1960
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
1961
+ allowsEditing: true,
1962
+ aspect: [1, 1],
1963
+ quality: 0.8,
1964
+ });
1965
+
1966
+ if (!result.canceled && result.assets[0]) {
1967
+ setImageUri(result.assets[0].uri);
1968
+ setError(null);
1969
+ }
1970
+ } catch (err) {
1971
+ setError('Failed to pick image');
1972
+ }
1973
+ };
1974
+
1975
+ const takePhoto = async () => {
1976
+ try {
1977
+ const result = await ImagePicker.launchCameraAsync({
1978
+ allowsEditing: true,
1979
+ aspect: [1, 1],
1980
+ quality: 0.8,
1981
+ });
1982
+
1983
+ if (!result.canceled && result.assets[0]) {
1984
+ setImageUri(result.assets[0].uri);
1985
+ setError(null);
1986
+ }
1987
+ } catch (err) {
1988
+ setError('Failed to take photo');
1989
+ }
1990
+ };
1991
+
1992
+ const handleUpload = async () => {
1993
+ if (!imageUri) return;
1994
+
1995
+ setIsUploading(true);
1996
+ setUploadProgress(0);
1997
+ setError(null);
1998
+
1999
+ try {
2000
+ // Simulate upload progress
2001
+ const progressInterval = setInterval(() => {
2002
+ setUploadProgress((prev) => Math.min(prev + 10, 90));
2003
+ }, 200);
2004
+
2005
+ const uploadedUrl = await onUpload(imageUri);
2006
+
2007
+ clearInterval(progressInterval);
2008
+ setUploadProgress(100);
2009
+ setImageUri(uploadedUrl);
2010
+ } catch (err) {
2011
+ setError(err instanceof Error ? err.message : 'Upload failed');
2012
+ } finally {
2013
+ setIsUploading(false);
2014
+ }
2015
+ };
2016
+
2017
+ const removeImage = () => {
2018
+ setImageUri(null);
2019
+ setUploadProgress(0);
2020
+ setError(null);
2021
+ };
2022
+
2023
+ return (
2024
+ <Card padding="lg">
2025
+ <Text variant="title" style={{ marginBottom: 16 }}>
2026
+ Profile Photo
2027
+ </Text>
2028
+
2029
+ {/* Image Preview */}
2030
+ <View style={{ alignItems: 'center', marginBottom: 16 }}>
2031
+ {imageUri ? (
2032
+ <View style={{ position: 'relative' }}>
2033
+ <Image
2034
+ source={{ uri: imageUri }}
2035
+ style={{
2036
+ width: 150,
2037
+ height: 150,
2038
+ borderRadius: 75,
2039
+ }}
2040
+ />
2041
+ <Pressable
2042
+ onPress={removeImage}
2043
+ style={{
2044
+ position: 'absolute',
2045
+ top: 0,
2046
+ right: 0,
2047
+ backgroundColor: 'rgba(0,0,0,0.6)',
2048
+ borderRadius: 12,
2049
+ padding: 4,
2050
+ }}
2051
+ >
2052
+ <Icon name="close" size={16} color="#fff" />
2053
+ </Pressable>
2054
+ </View>
2055
+ ) : (
2056
+ <View
2057
+ style={{
2058
+ width: 150,
2059
+ height: 150,
2060
+ borderRadius: 75,
2061
+ backgroundColor: 'rgba(0,0,0,0.1)',
2062
+ justifyContent: 'center',
2063
+ alignItems: 'center',
2064
+ }}
2065
+ >
2066
+ <Icon name="account" size={64} style={{ opacity: 0.3 }} />
2067
+ </View>
2068
+ )}
2069
+ </View>
2070
+
2071
+ {/* Upload Progress */}
2072
+ {isUploading && (
2073
+ <View style={{ marginBottom: 16 }}>
2074
+ <Progress value={uploadProgress} />
2075
+ <Text size="sm" style={{ textAlign: 'center', marginTop: 4 }}>
2076
+ Uploading... {uploadProgress}%
2077
+ </Text>
2078
+ </View>
2079
+ )}
2080
+
2081
+ {/* Error Message */}
2082
+ {error && (
2083
+ <Text intent="danger" style={{ textAlign: 'center', marginBottom: 16 }}>
2084
+ {error}
2085
+ </Text>
2086
+ )}
2087
+
2088
+ {/* Action Buttons */}
2089
+ <View style={{ gap: 12 }}>
2090
+ <View style={{ flexDirection: 'row', gap: 12 }}>
2091
+ <Button
2092
+ type="outlined"
2093
+ onPress={pickImage}
2094
+ disabled={isUploading}
2095
+ style={{ flex: 1 }}
2096
+ >
2097
+ <Icon name="image" size={18} /> Gallery
2098
+ </Button>
2099
+ <Button
2100
+ type="outlined"
2101
+ onPress={takePhoto}
2102
+ disabled={isUploading}
2103
+ style={{ flex: 1 }}
2104
+ >
2105
+ <Icon name="camera" size={18} /> Camera
2106
+ </Button>
2107
+ </View>
2108
+
2109
+ {imageUri && !imageUri.startsWith('http') && (
2110
+ <Button
2111
+ onPress={handleUpload}
2112
+ loading={isUploading}
2113
+ disabled={isUploading}
2114
+ >
2115
+ Upload Photo
2116
+ </Button>
2117
+ )}
2118
+ </View>
2119
+ </Card>
2120
+ );
2121
+ }
2122
+
2123
+ // Usage
2124
+ function ProfileScreen() {
2125
+ const uploadImage = async (uri: string): Promise<string> => {
2126
+ // Upload to your server/cloud storage
2127
+ const formData = new FormData();
2128
+ formData.append('image', {
2129
+ uri,
2130
+ type: 'image/jpeg',
2131
+ name: 'photo.jpg',
2132
+ } as any);
2133
+
2134
+ const response = await fetch('/api/upload', {
2135
+ method: 'POST',
2136
+ body: formData,
2137
+ });
2138
+
2139
+ const { url } = await response.json();
2140
+ return url;
2141
+ };
2142
+
2143
+ return (
2144
+ <ImageUpload
2145
+ currentImage="https://example.com/current-avatar.jpg"
2146
+ onUpload={uploadImage}
2147
+ />
2148
+ );
2149
+ }`,
2150
+ explanation: `This image upload component provides:
2151
+ - Pick from gallery or take photo
2152
+ - Image preview with circular crop
2153
+ - Upload progress indicator
2154
+ - Error handling
2155
+ - Remove/replace image option
2156
+ - Works with any backend upload API`,
2157
+ tips: [
2158
+ "Add image compression before upload to reduce size",
2159
+ "Consider using a CDN for image hosting",
2160
+ "Implement retry logic for failed uploads",
2161
+ ],
2162
+ relatedRecipes: ["form-with-validation"],
2163
+ },
2164
+ };
2165
+ /**
2166
+ * Get all recipes grouped by category
2167
+ */
2168
+ export function getRecipesByCategory() {
2169
+ const grouped = {};
2170
+ for (const recipe of Object.values(recipes)) {
2171
+ if (!grouped[recipe.category]) {
2172
+ grouped[recipe.category] = [];
2173
+ }
2174
+ grouped[recipe.category].push(recipe);
2175
+ }
2176
+ return grouped;
2177
+ }
2178
+ /**
2179
+ * Get a summary list of all recipes
2180
+ */
2181
+ export function getRecipeSummary() {
2182
+ return Object.entries(recipes).map(([id, recipe]) => ({
2183
+ id,
2184
+ name: recipe.name,
2185
+ description: recipe.description,
2186
+ category: recipe.category,
2187
+ difficulty: recipe.difficulty,
2188
+ packages: recipe.packages,
2189
+ }));
2190
+ }
2191
+ /**
2192
+ * Search recipes by query
2193
+ */
2194
+ export function searchRecipes(query) {
2195
+ const lowerQuery = query.toLowerCase();
2196
+ return Object.values(recipes).filter((recipe) => recipe.name.toLowerCase().includes(lowerQuery) ||
2197
+ recipe.description.toLowerCase().includes(lowerQuery) ||
2198
+ recipe.category.toLowerCase().includes(lowerQuery) ||
2199
+ recipe.packages.some((p) => p.toLowerCase().includes(lowerQuery)));
2200
+ }
2201
+ //# sourceMappingURL=recipes.js.map