@idealyst/mcp-server 1.2.24 → 1.2.26

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.
Files changed (193) hide show
  1. package/dist/index.cjs +22366 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.cts +1 -0
  4. package/dist/index.d.ts +0 -2
  5. package/dist/index.js +22186 -1034
  6. package/dist/index.js.map +1 -1
  7. package/package.json +17 -7
  8. package/dist/data/cli-commands.d.ts +0 -2
  9. package/dist/data/cli-commands.d.ts.map +0 -1
  10. package/dist/data/cli-commands.js +0 -100
  11. package/dist/data/cli-commands.js.map +0 -1
  12. package/dist/data/components/Accordion.d.ts +0 -15
  13. package/dist/data/components/Accordion.d.ts.map +0 -1
  14. package/dist/data/components/Accordion.js +0 -113
  15. package/dist/data/components/Accordion.js.map +0 -1
  16. package/dist/data/components/ActivityIndicator.d.ts +0 -15
  17. package/dist/data/components/ActivityIndicator.d.ts.map +0 -1
  18. package/dist/data/components/ActivityIndicator.js +0 -80
  19. package/dist/data/components/ActivityIndicator.js.map +0 -1
  20. package/dist/data/components/Alert.d.ts +0 -15
  21. package/dist/data/components/Alert.d.ts.map +0 -1
  22. package/dist/data/components/Alert.js +0 -130
  23. package/dist/data/components/Alert.js.map +0 -1
  24. package/dist/data/components/Avatar.d.ts +0 -15
  25. package/dist/data/components/Avatar.d.ts.map +0 -1
  26. package/dist/data/components/Avatar.js +0 -91
  27. package/dist/data/components/Avatar.js.map +0 -1
  28. package/dist/data/components/Badge.d.ts +0 -15
  29. package/dist/data/components/Badge.d.ts.map +0 -1
  30. package/dist/data/components/Badge.js +0 -64
  31. package/dist/data/components/Badge.js.map +0 -1
  32. package/dist/data/components/Breadcrumb.d.ts +0 -15
  33. package/dist/data/components/Breadcrumb.d.ts.map +0 -1
  34. package/dist/data/components/Breadcrumb.js +0 -92
  35. package/dist/data/components/Breadcrumb.js.map +0 -1
  36. package/dist/data/components/Button.d.ts +0 -16
  37. package/dist/data/components/Button.d.ts.map +0 -1
  38. package/dist/data/components/Button.js +0 -118
  39. package/dist/data/components/Button.js.map +0 -1
  40. package/dist/data/components/Card.d.ts +0 -15
  41. package/dist/data/components/Card.d.ts.map +0 -1
  42. package/dist/data/components/Card.js +0 -75
  43. package/dist/data/components/Card.js.map +0 -1
  44. package/dist/data/components/Checkbox.d.ts +0 -15
  45. package/dist/data/components/Checkbox.d.ts.map +0 -1
  46. package/dist/data/components/Checkbox.js +0 -118
  47. package/dist/data/components/Checkbox.js.map +0 -1
  48. package/dist/data/components/Chip.d.ts +0 -15
  49. package/dist/data/components/Chip.d.ts.map +0 -1
  50. package/dist/data/components/Chip.js +0 -94
  51. package/dist/data/components/Chip.js.map +0 -1
  52. package/dist/data/components/Dialog.d.ts +0 -15
  53. package/dist/data/components/Dialog.d.ts.map +0 -1
  54. package/dist/data/components/Dialog.js +0 -137
  55. package/dist/data/components/Dialog.js.map +0 -1
  56. package/dist/data/components/Divider.d.ts +0 -15
  57. package/dist/data/components/Divider.d.ts.map +0 -1
  58. package/dist/data/components/Divider.js +0 -68
  59. package/dist/data/components/Divider.js.map +0 -1
  60. package/dist/data/components/Icon.d.ts +0 -15
  61. package/dist/data/components/Icon.d.ts.map +0 -1
  62. package/dist/data/components/Icon.js +0 -68
  63. package/dist/data/components/Icon.js.map +0 -1
  64. package/dist/data/components/Image.d.ts +0 -15
  65. package/dist/data/components/Image.d.ts.map +0 -1
  66. package/dist/data/components/Image.js +0 -119
  67. package/dist/data/components/Image.js.map +0 -1
  68. package/dist/data/components/Input.d.ts +0 -15
  69. package/dist/data/components/Input.d.ts.map +0 -1
  70. package/dist/data/components/Input.js +0 -155
  71. package/dist/data/components/Input.js.map +0 -1
  72. package/dist/data/components/Link.d.ts +0 -15
  73. package/dist/data/components/Link.d.ts.map +0 -1
  74. package/dist/data/components/Link.js +0 -142
  75. package/dist/data/components/Link.js.map +0 -1
  76. package/dist/data/components/List.d.ts +0 -15
  77. package/dist/data/components/List.d.ts.map +0 -1
  78. package/dist/data/components/List.js +0 -113
  79. package/dist/data/components/List.js.map +0 -1
  80. package/dist/data/components/Menu.d.ts +0 -15
  81. package/dist/data/components/Menu.d.ts.map +0 -1
  82. package/dist/data/components/Menu.js +0 -123
  83. package/dist/data/components/Menu.js.map +0 -1
  84. package/dist/data/components/Popover.d.ts +0 -15
  85. package/dist/data/components/Popover.d.ts.map +0 -1
  86. package/dist/data/components/Popover.js +0 -157
  87. package/dist/data/components/Popover.js.map +0 -1
  88. package/dist/data/components/Pressable.d.ts +0 -15
  89. package/dist/data/components/Pressable.d.ts.map +0 -1
  90. package/dist/data/components/Pressable.js +0 -125
  91. package/dist/data/components/Pressable.js.map +0 -1
  92. package/dist/data/components/Progress.d.ts +0 -15
  93. package/dist/data/components/Progress.d.ts.map +0 -1
  94. package/dist/data/components/Progress.js +0 -93
  95. package/dist/data/components/Progress.js.map +0 -1
  96. package/dist/data/components/RadioButton.d.ts +0 -15
  97. package/dist/data/components/RadioButton.d.ts.map +0 -1
  98. package/dist/data/components/RadioButton.js +0 -131
  99. package/dist/data/components/RadioButton.js.map +0 -1
  100. package/dist/data/components/SVGImage.d.ts +0 -15
  101. package/dist/data/components/SVGImage.d.ts.map +0 -1
  102. package/dist/data/components/SVGImage.js +0 -112
  103. package/dist/data/components/SVGImage.js.map +0 -1
  104. package/dist/data/components/Screen.d.ts +0 -15
  105. package/dist/data/components/Screen.d.ts.map +0 -1
  106. package/dist/data/components/Screen.js +0 -109
  107. package/dist/data/components/Screen.js.map +0 -1
  108. package/dist/data/components/Select.d.ts +0 -15
  109. package/dist/data/components/Select.d.ts.map +0 -1
  110. package/dist/data/components/Select.js +0 -141
  111. package/dist/data/components/Select.js.map +0 -1
  112. package/dist/data/components/Skeleton.d.ts +0 -15
  113. package/dist/data/components/Skeleton.d.ts.map +0 -1
  114. package/dist/data/components/Skeleton.js +0 -100
  115. package/dist/data/components/Skeleton.js.map +0 -1
  116. package/dist/data/components/Slider.d.ts +0 -15
  117. package/dist/data/components/Slider.d.ts.map +0 -1
  118. package/dist/data/components/Slider.js +0 -151
  119. package/dist/data/components/Slider.js.map +0 -1
  120. package/dist/data/components/Switch.d.ts +0 -15
  121. package/dist/data/components/Switch.d.ts.map +0 -1
  122. package/dist/data/components/Switch.js +0 -128
  123. package/dist/data/components/Switch.js.map +0 -1
  124. package/dist/data/components/TabBar.d.ts +0 -17
  125. package/dist/data/components/TabBar.d.ts.map +0 -1
  126. package/dist/data/components/TabBar.js +0 -244
  127. package/dist/data/components/TabBar.js.map +0 -1
  128. package/dist/data/components/Table.d.ts +0 -15
  129. package/dist/data/components/Table.d.ts.map +0 -1
  130. package/dist/data/components/Table.js +0 -159
  131. package/dist/data/components/Table.js.map +0 -1
  132. package/dist/data/components/Tabs.d.ts +0 -15
  133. package/dist/data/components/Tabs.d.ts.map +0 -1
  134. package/dist/data/components/Tabs.js +0 -150
  135. package/dist/data/components/Tabs.js.map +0 -1
  136. package/dist/data/components/Text.d.ts +0 -15
  137. package/dist/data/components/Text.d.ts.map +0 -1
  138. package/dist/data/components/Text.js +0 -97
  139. package/dist/data/components/Text.js.map +0 -1
  140. package/dist/data/components/TextArea.d.ts +0 -15
  141. package/dist/data/components/TextArea.d.ts.map +0 -1
  142. package/dist/data/components/TextArea.js +0 -156
  143. package/dist/data/components/TextArea.js.map +0 -1
  144. package/dist/data/components/Tooltip.d.ts +0 -15
  145. package/dist/data/components/Tooltip.d.ts.map +0 -1
  146. package/dist/data/components/Tooltip.js +0 -103
  147. package/dist/data/components/Tooltip.js.map +0 -1
  148. package/dist/data/components/Video.d.ts +0 -15
  149. package/dist/data/components/Video.d.ts.map +0 -1
  150. package/dist/data/components/Video.js +0 -166
  151. package/dist/data/components/Video.js.map +0 -1
  152. package/dist/data/components/View.d.ts +0 -15
  153. package/dist/data/components/View.d.ts.map +0 -1
  154. package/dist/data/components/View.js +0 -127
  155. package/dist/data/components/View.js.map +0 -1
  156. package/dist/data/components/index.d.ts +0 -38
  157. package/dist/data/components/index.d.ts.map +0 -1
  158. package/dist/data/components/index.js +0 -113
  159. package/dist/data/components/index.js.map +0 -1
  160. package/dist/data/framework-guides.d.ts +0 -2
  161. package/dist/data/framework-guides.d.ts.map +0 -1
  162. package/dist/data/framework-guides.js +0 -1730
  163. package/dist/data/framework-guides.js.map +0 -1
  164. package/dist/data/icon-guide.d.ts +0 -2
  165. package/dist/data/icon-guide.d.ts.map +0 -1
  166. package/dist/data/icon-guide.js +0 -285
  167. package/dist/data/icon-guide.js.map +0 -1
  168. package/dist/data/icons.json +0 -7452
  169. package/dist/data/navigation-guides.d.ts +0 -2
  170. package/dist/data/navigation-guides.d.ts.map +0 -1
  171. package/dist/data/navigation-guides.js +0 -2144
  172. package/dist/data/navigation-guides.js.map +0 -1
  173. package/dist/data/packages.d.ts +0 -39
  174. package/dist/data/packages.d.ts.map +0 -1
  175. package/dist/data/packages.js +0 -550
  176. package/dist/data/packages.js.map +0 -1
  177. package/dist/data/recipes.d.ts +0 -36
  178. package/dist/data/recipes.d.ts.map +0 -1
  179. package/dist/data/recipes.js +0 -2945
  180. package/dist/data/recipes.js.map +0 -1
  181. package/dist/data/storage-guides.d.ts +0 -2
  182. package/dist/data/storage-guides.d.ts.map +0 -1
  183. package/dist/data/storage-guides.js +0 -418
  184. package/dist/data/storage-guides.js.map +0 -1
  185. package/dist/data/translate-guides.d.ts +0 -2
  186. package/dist/data/translate-guides.d.ts.map +0 -1
  187. package/dist/data/translate-guides.js +0 -1030
  188. package/dist/data/translate-guides.js.map +0 -1
  189. package/dist/index.d.ts.map +0 -1
  190. package/dist/tools/get-types.d.ts +0 -37
  191. package/dist/tools/get-types.d.ts.map +0 -1
  192. package/dist/tools/get-types.js +0 -148
  193. package/dist/tools/get-types.js.map +0 -1
@@ -1,2945 +0,0 @@
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
- "web-stack-layout": {
2165
- name: "Web Stack Layout",
2166
- description: "Stack layout for web that mimics native stack navigator with header, back button, and title",
2167
- category: "navigation",
2168
- difficulty: "intermediate",
2169
- packages: ["@idealyst/components", "@idealyst/navigation"],
2170
- code: `import React from 'react';
2171
- import { Outlet } from 'react-router-dom';
2172
- import { View, Text, Pressable, Icon } from '@idealyst/components';
2173
- import { useNavigator } from '@idealyst/navigation';
2174
- import type { StackLayoutProps, NavigatorOptions } from '@idealyst/navigation';
2175
-
2176
- /**
2177
- * Web Stack Layout - mimics native stack navigator header
2178
- *
2179
- * Features:
2180
- * - Header with title (from options.headerTitle)
2181
- * - Automatic back button when canGoBack() is true
2182
- * - Left and right header slots
2183
- * - Hide header with headerShown: false
2184
- */
2185
- export function WebStackLayout({ options }: StackLayoutProps) {
2186
- const { canGoBack, goBack } = useNavigator();
2187
-
2188
- const showHeader = options?.headerShown !== false;
2189
- const showBackButton = options?.headerBackVisible !== false && canGoBack();
2190
-
2191
- return (
2192
- <View style={{ flex: 1 }}>
2193
- {/* Header bar - like native stack header */}
2194
- {showHeader && (
2195
- <View style={{
2196
- height: 56,
2197
- flexDirection: 'row',
2198
- alignItems: 'center',
2199
- paddingHorizontal: 8,
2200
- backgroundColor: '#ffffff',
2201
- borderBottomWidth: 1,
2202
- borderBottomColor: '#e0e0e0',
2203
- // Web shadow
2204
- boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
2205
- }}>
2206
- {/* Back button */}
2207
- {showBackButton && (
2208
- <Pressable
2209
- onPress={goBack}
2210
- style={{
2211
- width: 40,
2212
- height: 40,
2213
- alignItems: 'center',
2214
- justifyContent: 'center',
2215
- borderRadius: 20,
2216
- }}
2217
- >
2218
- <Icon name="arrow-left" size={24} />
2219
- </Pressable>
2220
- )}
2221
-
2222
- {/* Left header slot */}
2223
- {options?.headerLeft && (
2224
- <View style={{ marginLeft: 8 }}>
2225
- {renderHeaderComponent(options.headerLeft)}
2226
- </View>
2227
- )}
2228
-
2229
- {/* Title */}
2230
- <View style={{ flex: 1, marginHorizontal: 8 }}>
2231
- {typeof options?.headerTitle === 'string' ? (
2232
- <Text variant="title" numberOfLines={1}>
2233
- {options.headerTitle}
2234
- </Text>
2235
- ) : options?.headerTitle ? (
2236
- renderHeaderComponent(options.headerTitle)
2237
- ) : null}
2238
- </View>
2239
-
2240
- {/* Right header slot */}
2241
- {options?.headerRight && (
2242
- <View>
2243
- {renderHeaderComponent(options.headerRight)}
2244
- </View>
2245
- )}
2246
- </View>
2247
- )}
2248
-
2249
- {/* Content area - Outlet renders child routes */}
2250
- <View style={{ flex: 1 }}>
2251
- <Outlet />
2252
- </View>
2253
- </View>
2254
- );
2255
- }
2256
-
2257
- // Helper to render header components (can be React element or function)
2258
- function renderHeaderComponent(
2259
- component: React.ComponentType | React.ReactElement | undefined
2260
- ) {
2261
- if (!component) return null;
2262
- if (typeof component === 'function') {
2263
- const Component = component;
2264
- return <Component />;
2265
- }
2266
- return component;
2267
- }
2268
-
2269
- // Usage in router config:
2270
- // {
2271
- // path: "/",
2272
- // type: 'navigator',
2273
- // layout: 'stack',
2274
- // layoutComponent: WebStackLayout,
2275
- // options: {
2276
- // headerTitle: "My App",
2277
- // headerRight: <UserMenu />,
2278
- // },
2279
- // routes: [...]
2280
- // }`,
2281
- explanation: `This layout mimics the native stack navigator header:
2282
-
2283
- **What it provides:**
2284
- - Fixed header bar at the top (56px height like native)
2285
- - Automatic back button that appears when canGoBack() returns true
2286
- - headerTitle renders as text or custom component
2287
- - headerLeft and headerRight slots for custom actions
2288
- - headerShown: false hides the entire header
2289
- - headerBackVisible: false hides just the back button
2290
-
2291
- **Key insight:** On web, canGoBack() checks if there's a parent route in the hierarchy, not browser history. This matches the native behavior where back goes "up" the navigation stack.`,
2292
- tips: [
2293
- "Use options.headerShown: false for fullscreen content like media players",
2294
- "The back button appears automatically - no need to manage visibility",
2295
- "headerRight is great for action buttons, user menus, or search",
2296
- "Wrap your entire app router with this for consistent headers",
2297
- ],
2298
- relatedRecipes: ["web-tab-layout", "web-drawer-layout", "responsive-navigation"],
2299
- },
2300
- "web-tab-layout": {
2301
- name: "Web Tab Layout",
2302
- description: "Tab layout for web that mimics native bottom tab navigator with icons, labels, and badges",
2303
- category: "navigation",
2304
- difficulty: "intermediate",
2305
- packages: ["@idealyst/components", "@idealyst/navigation"],
2306
- code: `import React from 'react';
2307
- import { Outlet } from 'react-router-dom';
2308
- import { View, Text, Pressable, Icon, Badge } from '@idealyst/components';
2309
- import { useNavigator } from '@idealyst/navigation';
2310
- import type { TabLayoutProps } from '@idealyst/navigation';
2311
-
2312
- /**
2313
- * Web Tab Layout - mimics native bottom tab navigator
2314
- *
2315
- * Features:
2316
- * - Bottom tab bar (like iOS/Android)
2317
- * - Icons from tabBarIcon option
2318
- * - Labels from tabBarLabel option
2319
- * - Badge counts from tabBarBadge option
2320
- * - Active state highlighting
2321
- */
2322
- export function WebTabLayout({ routes, currentPath }: TabLayoutProps) {
2323
- const { navigate } = useNavigator();
2324
-
2325
- return (
2326
- <View style={{ flex: 1 }}>
2327
- {/* Content area - takes remaining space */}
2328
- <View style={{ flex: 1 }}>
2329
- <Outlet />
2330
- </View>
2331
-
2332
- {/* Bottom tab bar */}
2333
- <View style={{
2334
- height: 56,
2335
- flexDirection: 'row',
2336
- backgroundColor: '#ffffff',
2337
- borderTopWidth: 1,
2338
- borderTopColor: '#e0e0e0',
2339
- // Safe area padding for mobile web
2340
- paddingBottom: 'env(safe-area-inset-bottom, 0px)',
2341
- }}>
2342
- {routes.map((route) => {
2343
- // Check if this tab is active
2344
- const isActive = currentPath === route.fullPath ||
2345
- currentPath.startsWith(route.fullPath + '/');
2346
-
2347
- const tabOptions = route.options;
2348
- const activeColor = '#007AFF';
2349
- const inactiveColor = '#8E8E93';
2350
-
2351
- return (
2352
- <Pressable
2353
- key={route.fullPath}
2354
- onPress={() => navigate({ path: route.fullPath })}
2355
- style={{
2356
- flex: 1,
2357
- alignItems: 'center',
2358
- justifyContent: 'center',
2359
- paddingVertical: 4,
2360
- }}
2361
- >
2362
- {/* Icon container with badge */}
2363
- <View style={{ position: 'relative' }}>
2364
- {tabOptions?.tabBarIcon?.({
2365
- focused: isActive,
2366
- color: isActive ? activeColor : inactiveColor,
2367
- size: 24,
2368
- })}
2369
-
2370
- {/* Badge */}
2371
- {tabOptions?.tabBarBadge != null && (
2372
- <View style={{
2373
- position: 'absolute',
2374
- top: -4,
2375
- right: -12,
2376
- minWidth: 18,
2377
- height: 18,
2378
- borderRadius: 9,
2379
- backgroundColor: '#FF3B30',
2380
- alignItems: 'center',
2381
- justifyContent: 'center',
2382
- paddingHorizontal: 4,
2383
- }}>
2384
- <Text style={{ color: '#fff', fontSize: 10, fontWeight: '600' }}>
2385
- {typeof tabOptions.tabBarBadge === 'number' && tabOptions.tabBarBadge > 99
2386
- ? '99+'
2387
- : tabOptions.tabBarBadge}
2388
- </Text>
2389
- </View>
2390
- )}
2391
- </View>
2392
-
2393
- {/* Label */}
2394
- {tabOptions?.tabBarLabel && (
2395
- <Text
2396
- size="xs"
2397
- style={{
2398
- marginTop: 2,
2399
- color: isActive ? activeColor : inactiveColor,
2400
- fontWeight: isActive ? '600' : '400',
2401
- }}
2402
- >
2403
- {tabOptions.tabBarLabel}
2404
- </Text>
2405
- )}
2406
- </Pressable>
2407
- );
2408
- })}
2409
- </View>
2410
- </View>
2411
- );
2412
- }
2413
-
2414
- // Usage in router config:
2415
- // {
2416
- // path: "/main",
2417
- // type: 'navigator',
2418
- // layout: 'tab',
2419
- // layoutComponent: WebTabLayout,
2420
- // routes: [
2421
- // {
2422
- // path: "home",
2423
- // type: 'screen',
2424
- // component: HomeScreen,
2425
- // options: {
2426
- // tabBarLabel: "Home",
2427
- // tabBarIcon: ({ focused, color }) => (
2428
- // <Icon name={focused ? "home" : "home-outline"} color={color} />
2429
- // ),
2430
- // },
2431
- // },
2432
- // {
2433
- // path: "notifications",
2434
- // type: 'screen',
2435
- // component: NotificationsScreen,
2436
- // options: {
2437
- // tabBarLabel: "Notifications",
2438
- // tabBarIcon: ({ color }) => <Icon name="bell" color={color} />,
2439
- // tabBarBadge: 5,
2440
- // },
2441
- // },
2442
- // ],
2443
- // }`,
2444
- explanation: `This layout mimics the native bottom tab bar:
2445
-
2446
- **What routes provide:**
2447
- - \`route.fullPath\` - The complete path for navigation
2448
- - \`route.options.tabBarIcon\` - Function that receives { focused, color, size }
2449
- - \`route.options.tabBarLabel\` - Text label for the tab
2450
- - \`route.options.tabBarBadge\` - Badge count (number or string)
2451
-
2452
- **Active state detection:**
2453
- \`currentPath === route.fullPath\` - Exact match for active state
2454
- Or use \`startsWith\` for nested routes under a tab
2455
-
2456
- **Platform parity:**
2457
- - 56px tab bar height matches iOS/Android
2458
- - Icon + label layout matches native patterns
2459
- - Badge styling matches iOS notification badges
2460
- - Safe area inset for mobile web browsers`,
2461
- tips: [
2462
- "Use outlined/filled icon variants for focused state (home-outline vs home)",
2463
- "Keep tab count to 3-5 for best usability",
2464
- "Badge counts over 99 should show '99+' to fit",
2465
- "Consider hiding the tab bar on detail screens with conditional rendering",
2466
- ],
2467
- relatedRecipes: ["web-stack-layout", "tab-navigation", "responsive-navigation"],
2468
- },
2469
- "web-drawer-layout": {
2470
- name: "Web Drawer/Sidebar Layout",
2471
- description: "Sidebar layout for web that provides persistent navigation menu with collapsible support",
2472
- category: "navigation",
2473
- difficulty: "intermediate",
2474
- packages: ["@idealyst/components", "@idealyst/navigation"],
2475
- code: `import React, { useState } from 'react';
2476
- import { Outlet } from 'react-router-dom';
2477
- import { View, Text, Pressable, Icon, Avatar } from '@idealyst/components';
2478
- import { useNavigator } from '@idealyst/navigation';
2479
- import type { StackLayoutProps } from '@idealyst/navigation';
2480
-
2481
- interface DrawerLayoutOptions {
2482
- expandedWidth?: number;
2483
- collapsedWidth?: number;
2484
- initiallyCollapsed?: boolean;
2485
- }
2486
-
2487
- /**
2488
- * Web Drawer Layout - persistent sidebar navigation
2489
- *
2490
- * Features:
2491
- * - Collapsible sidebar with smooth animation
2492
- * - Active route highlighting
2493
- * - Icon + label navigation items
2494
- * - User profile section
2495
- * - Works with any navigator layout type
2496
- */
2497
- export function WebDrawerLayout({
2498
- routes,
2499
- currentPath,
2500
- options,
2501
- }: StackLayoutProps & { drawerOptions?: DrawerLayoutOptions }) {
2502
- const { navigate } = useNavigator();
2503
- const [isCollapsed, setIsCollapsed] = useState(false);
2504
-
2505
- const expandedWidth = 240;
2506
- const collapsedWidth = 64;
2507
- const sidebarWidth = isCollapsed ? collapsedWidth : expandedWidth;
2508
-
2509
- return (
2510
- <View style={{ flex: 1, flexDirection: 'row' }}>
2511
- {/* Sidebar */}
2512
- <View style={{
2513
- width: sidebarWidth,
2514
- backgroundColor: '#1a1a2e',
2515
- transition: 'width 0.2s ease',
2516
- overflow: 'hidden',
2517
- }}>
2518
- {/* Logo / App Header */}
2519
- <View style={{
2520
- height: 64,
2521
- flexDirection: 'row',
2522
- alignItems: 'center',
2523
- paddingHorizontal: 16,
2524
- borderBottomWidth: 1,
2525
- borderBottomColor: 'rgba(255,255,255,0.1)',
2526
- }}>
2527
- <Icon name="rocket" size={28} color="#fff" />
2528
- {!isCollapsed && (
2529
- <Text
2530
- variant="title"
2531
- style={{ color: '#fff', marginLeft: 12 }}
2532
- >
2533
- {options?.headerTitle || 'My App'}
2534
- </Text>
2535
- )}
2536
- </View>
2537
-
2538
- {/* Navigation Items */}
2539
- <View style={{ flex: 1, paddingVertical: 8 }}>
2540
- {routes.map((route) => {
2541
- const isActive = currentPath === route.fullPath ||
2542
- currentPath.startsWith(route.fullPath + '/');
2543
-
2544
- return (
2545
- <Pressable
2546
- key={route.fullPath}
2547
- onPress={() => navigate({ path: route.fullPath })}
2548
- style={{
2549
- flexDirection: 'row',
2550
- alignItems: 'center',
2551
- paddingVertical: 12,
2552
- paddingHorizontal: 16,
2553
- marginHorizontal: 8,
2554
- marginVertical: 2,
2555
- borderRadius: 8,
2556
- backgroundColor: isActive ? 'rgba(255,255,255,0.15)' : 'transparent',
2557
- }}
2558
- >
2559
- <Icon
2560
- name={route.options?.icon || 'circle'}
2561
- size={24}
2562
- color={isActive ? '#fff' : 'rgba(255,255,255,0.6)'}
2563
- />
2564
- {!isCollapsed && (
2565
- <Text
2566
- style={{
2567
- marginLeft: 12,
2568
- color: isActive ? '#fff' : 'rgba(255,255,255,0.6)',
2569
- fontWeight: isActive ? '600' : '400',
2570
- }}
2571
- >
2572
- {route.options?.title || route.path}
2573
- </Text>
2574
- )}
2575
- </Pressable>
2576
- );
2577
- })}
2578
- </View>
2579
-
2580
- {/* User Section (optional) */}
2581
- <View style={{
2582
- padding: 16,
2583
- borderTopWidth: 1,
2584
- borderTopColor: 'rgba(255,255,255,0.1)',
2585
- }}>
2586
- <Pressable
2587
- style={{ flexDirection: 'row', alignItems: 'center' }}
2588
- onPress={() => navigate({ path: '/profile' })}
2589
- >
2590
- <Avatar size="sm" />
2591
- {!isCollapsed && (
2592
- <View style={{ marginLeft: 12 }}>
2593
- <Text style={{ color: '#fff', fontWeight: '500' }}>John Doe</Text>
2594
- <Text size="xs" style={{ color: 'rgba(255,255,255,0.5)' }}>
2595
- View profile
2596
- </Text>
2597
- </View>
2598
- )}
2599
- </Pressable>
2600
- </View>
2601
-
2602
- {/* Collapse Toggle */}
2603
- <Pressable
2604
- onPress={() => setIsCollapsed(!isCollapsed)}
2605
- style={{
2606
- padding: 16,
2607
- borderTopWidth: 1,
2608
- borderTopColor: 'rgba(255,255,255,0.1)',
2609
- flexDirection: 'row',
2610
- alignItems: 'center',
2611
- justifyContent: isCollapsed ? 'center' : 'flex-start',
2612
- }}
2613
- >
2614
- <Icon
2615
- name={isCollapsed ? 'chevron-right' : 'chevron-left'}
2616
- size={20}
2617
- color="rgba(255,255,255,0.6)"
2618
- />
2619
- {!isCollapsed && (
2620
- <Text style={{ marginLeft: 12, color: 'rgba(255,255,255,0.6)' }}>
2621
- Collapse
2622
- </Text>
2623
- )}
2624
- </Pressable>
2625
- </View>
2626
-
2627
- {/* Main Content */}
2628
- <View style={{ flex: 1, backgroundColor: '#f5f5f5' }}>
2629
- <Outlet />
2630
- </View>
2631
- </View>
2632
- );
2633
- }
2634
-
2635
- // Usage:
2636
- // {
2637
- // path: "/",
2638
- // type: 'navigator',
2639
- // layout: 'drawer', // or 'stack' - layout type doesn't matter for web
2640
- // layoutComponent: WebDrawerLayout,
2641
- // options: { headerTitle: "Dashboard" },
2642
- // routes: [
2643
- // { path: "home", component: Home, options: { title: "Home", icon: "home" } },
2644
- // { path: "users", component: Users, options: { title: "Users", icon: "account-group" } },
2645
- // { path: "settings", component: Settings, options: { title: "Settings", icon: "cog" } },
2646
- // ],
2647
- // }`,
2648
- explanation: `This sidebar layout is ideal for dashboards and admin panels:
2649
-
2650
- **Route options used:**
2651
- - \`route.options.title\` - Menu item label
2652
- - \`route.options.icon\` - Material Design Icon name
2653
-
2654
- **Features:**
2655
- - Collapsible sidebar with smooth width transition
2656
- - Active state with background highlight
2657
- - User profile section at bottom
2658
- - Collapse toggle button
2659
- - Dark theme (easily customizable)
2660
-
2661
- **Why this differs from mobile:**
2662
- On native, a drawer slides over content. On web, a persistent sidebar is more common and user-friendly. This layout provides the web-appropriate pattern while using the same route configuration.`,
2663
- tips: [
2664
- "Add tooltips on collapsed icons using the title attribute",
2665
- "Consider responsive behavior - hide sidebar on mobile, show bottom tabs instead",
2666
- "Use the icon option on routes to define sidebar icons",
2667
- "The collapse state could be persisted to localStorage",
2668
- ],
2669
- relatedRecipes: ["web-stack-layout", "responsive-navigation", "drawer-navigation"],
2670
- },
2671
- "responsive-navigation": {
2672
- name: "Responsive Navigation Layout",
2673
- description: "Layout that switches between bottom tabs on mobile and sidebar on desktop",
2674
- category: "navigation",
2675
- difficulty: "advanced",
2676
- packages: ["@idealyst/components", "@idealyst/navigation"],
2677
- code: `import React, { useState, useEffect } from 'react';
2678
- import { Outlet } from 'react-router-dom';
2679
- import { View, Text, Pressable, Icon } from '@idealyst/components';
2680
- import { useNavigator } from '@idealyst/navigation';
2681
- import type { StackLayoutProps } from '@idealyst/navigation';
2682
-
2683
- // Custom hook for responsive breakpoints
2684
- function useResponsive() {
2685
- const [width, setWidth] = useState(
2686
- typeof window !== 'undefined' ? window.innerWidth : 1024
2687
- );
2688
-
2689
- useEffect(() => {
2690
- const handleResize = () => setWidth(window.innerWidth);
2691
- window.addEventListener('resize', handleResize);
2692
- return () => window.removeEventListener('resize', handleResize);
2693
- }, []);
2694
-
2695
- return {
2696
- isMobile: width < 768,
2697
- isTablet: width >= 768 && width < 1024,
2698
- isDesktop: width >= 1024,
2699
- width,
2700
- };
2701
- }
2702
-
2703
- /**
2704
- * Responsive Navigation Layout
2705
- *
2706
- * - Mobile (<768px): Bottom tab bar
2707
- * - Desktop (>=768px): Sidebar navigation
2708
- *
2709
- * Uses the same route configuration for both!
2710
- */
2711
- export function ResponsiveNavLayout({ routes, currentPath, options }: StackLayoutProps) {
2712
- const { isMobile } = useResponsive();
2713
-
2714
- // Same routes power both layouts
2715
- return isMobile ? (
2716
- <MobileTabBar routes={routes} currentPath={currentPath} />
2717
- ) : (
2718
- <DesktopSidebar routes={routes} currentPath={currentPath} options={options} />
2719
- );
2720
- }
2721
-
2722
- // Mobile: Bottom tab bar
2723
- function MobileTabBar({ routes, currentPath }: StackLayoutProps) {
2724
- const { navigate } = useNavigator();
2725
-
2726
- return (
2727
- <View style={{ flex: 1 }}>
2728
- <View style={{ flex: 1 }}>
2729
- <Outlet />
2730
- </View>
2731
-
2732
- <View style={{
2733
- flexDirection: 'row',
2734
- height: 56,
2735
- backgroundColor: '#fff',
2736
- borderTopWidth: 1,
2737
- borderTopColor: '#e0e0e0',
2738
- paddingBottom: 'env(safe-area-inset-bottom, 0px)',
2739
- }}>
2740
- {routes.slice(0, 5).map((route) => {
2741
- const isActive = currentPath.startsWith(route.fullPath);
2742
- return (
2743
- <Pressable
2744
- key={route.fullPath}
2745
- onPress={() => navigate({ path: route.fullPath })}
2746
- style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}
2747
- >
2748
- <Icon
2749
- name={route.options?.icon || 'circle'}
2750
- size={24}
2751
- color={isActive ? '#007AFF' : '#8E8E93'}
2752
- />
2753
- <Text
2754
- size="xs"
2755
- style={{
2756
- marginTop: 2,
2757
- color: isActive ? '#007AFF' : '#8E8E93',
2758
- }}
2759
- >
2760
- {route.options?.tabBarLabel || route.options?.title}
2761
- </Text>
2762
- </Pressable>
2763
- );
2764
- })}
2765
- </View>
2766
- </View>
2767
- );
2768
- }
2769
-
2770
- // Desktop: Sidebar
2771
- function DesktopSidebar({ routes, currentPath, options }: StackLayoutProps) {
2772
- const { navigate } = useNavigator();
2773
- const [isCollapsed, setIsCollapsed] = useState(false);
2774
-
2775
- return (
2776
- <View style={{ flex: 1, flexDirection: 'row' }}>
2777
- {/* Sidebar */}
2778
- <View style={{
2779
- width: isCollapsed ? 64 : 240,
2780
- backgroundColor: '#1e1e2d',
2781
- transition: 'width 0.2s',
2782
- }}>
2783
- {/* Header */}
2784
- <View style={{
2785
- height: 64,
2786
- flexDirection: 'row',
2787
- alignItems: 'center',
2788
- paddingHorizontal: 16,
2789
- }}>
2790
- <Icon name="rocket" size={28} color="#fff" />
2791
- {!isCollapsed && (
2792
- <Text style={{ color: '#fff', marginLeft: 12, fontWeight: '600' }}>
2793
- {options?.headerTitle || 'App'}
2794
- </Text>
2795
- )}
2796
- </View>
2797
-
2798
- {/* Nav Items */}
2799
- <View style={{ flex: 1, paddingTop: 8 }}>
2800
- {routes.map((route) => {
2801
- const isActive = currentPath.startsWith(route.fullPath);
2802
- return (
2803
- <Pressable
2804
- key={route.fullPath}
2805
- onPress={() => navigate({ path: route.fullPath })}
2806
- style={{
2807
- flexDirection: 'row',
2808
- alignItems: 'center',
2809
- padding: 12,
2810
- marginHorizontal: 8,
2811
- marginVertical: 2,
2812
- borderRadius: 8,
2813
- backgroundColor: isActive ? 'rgba(255,255,255,0.1)' : 'transparent',
2814
- }}
2815
- >
2816
- <Icon
2817
- name={route.options?.icon || 'circle'}
2818
- size={24}
2819
- color={isActive ? '#fff' : 'rgba(255,255,255,0.6)'}
2820
- />
2821
- {!isCollapsed && (
2822
- <Text style={{
2823
- marginLeft: 12,
2824
- color: isActive ? '#fff' : 'rgba(255,255,255,0.6)',
2825
- }}>
2826
- {route.options?.title}
2827
- </Text>
2828
- )}
2829
- </Pressable>
2830
- );
2831
- })}
2832
- </View>
2833
-
2834
- {/* Collapse toggle */}
2835
- <Pressable
2836
- onPress={() => setIsCollapsed(!isCollapsed)}
2837
- style={{ padding: 16 }}
2838
- >
2839
- <Icon
2840
- name={isCollapsed ? 'menu' : 'menu-open'}
2841
- size={24}
2842
- color="rgba(255,255,255,0.6)"
2843
- />
2844
- </Pressable>
2845
- </View>
2846
-
2847
- {/* Content */}
2848
- <View style={{ flex: 1 }}>
2849
- <Outlet />
2850
- </View>
2851
- </View>
2852
- );
2853
- }
2854
-
2855
- // Usage - works with same routes for both mobile and desktop:
2856
- // {
2857
- // path: "/",
2858
- // type: 'navigator',
2859
- // layout: 'stack',
2860
- // layoutComponent: ResponsiveNavLayout,
2861
- // options: { headerTitle: "My App" },
2862
- // routes: [
2863
- // {
2864
- // path: "home",
2865
- // component: HomeScreen,
2866
- // options: {
2867
- // title: "Home", // Used by sidebar
2868
- // tabBarLabel: "Home", // Used by tab bar
2869
- // icon: "home", // Used by both
2870
- // },
2871
- // },
2872
- // {
2873
- // path: "search",
2874
- // component: SearchScreen,
2875
- // options: { title: "Search", tabBarLabel: "Search", icon: "magnify" },
2876
- // },
2877
- // // ... more routes
2878
- // ],
2879
- // }`,
2880
- explanation: `This layout automatically adapts to screen size:
2881
-
2882
- **Breakpoint logic:**
2883
- - Mobile (<768px): Bottom tab bar like native apps
2884
- - Desktop (>=768px): Persistent sidebar like web apps
2885
-
2886
- **Key insight:** The same route configuration powers both layouts! Routes define:
2887
- - \`title\` - Used by sidebar menu
2888
- - \`tabBarLabel\` - Used by tab bar (falls back to title)
2889
- - \`icon\` - Used by both
2890
-
2891
- **Why this matters:**
2892
- 1. Write routes once, render appropriately per device
2893
- 2. Users get the expected pattern for their device
2894
- 3. No need for separate mobile/desktop route configs
2895
- 4. Smooth transition when resizing browser
2896
-
2897
- **useResponsive hook:**
2898
- Custom hook that tracks window width and provides boolean flags for breakpoints. Could be extended with more breakpoints or use a library like react-responsive.`,
2899
- tips: [
2900
- "Limit mobile tabs to 5 items max (slice shown in code)",
2901
- "Consider adding a 'More' tab that opens a menu for additional items",
2902
- "Persist sidebar collapsed state to localStorage",
2903
- "Add transition animation when switching between layouts",
2904
- "Consider tablet-specific layout (sidebar + different styling)",
2905
- ],
2906
- relatedRecipes: ["web-stack-layout", "web-tab-layout", "web-drawer-layout"],
2907
- },
2908
- };
2909
- /**
2910
- * Get all recipes grouped by category
2911
- */
2912
- export function getRecipesByCategory() {
2913
- const grouped = {};
2914
- for (const recipe of Object.values(recipes)) {
2915
- if (!grouped[recipe.category]) {
2916
- grouped[recipe.category] = [];
2917
- }
2918
- grouped[recipe.category].push(recipe);
2919
- }
2920
- return grouped;
2921
- }
2922
- /**
2923
- * Get a summary list of all recipes
2924
- */
2925
- export function getRecipeSummary() {
2926
- return Object.entries(recipes).map(([id, recipe]) => ({
2927
- id,
2928
- name: recipe.name,
2929
- description: recipe.description,
2930
- category: recipe.category,
2931
- difficulty: recipe.difficulty,
2932
- packages: recipe.packages,
2933
- }));
2934
- }
2935
- /**
2936
- * Search recipes by query
2937
- */
2938
- export function searchRecipes(query) {
2939
- const lowerQuery = query.toLowerCase();
2940
- return Object.values(recipes).filter((recipe) => recipe.name.toLowerCase().includes(lowerQuery) ||
2941
- recipe.description.toLowerCase().includes(lowerQuery) ||
2942
- recipe.category.toLowerCase().includes(lowerQuery) ||
2943
- recipe.packages.some((p) => p.toLowerCase().includes(lowerQuery)));
2944
- }
2945
- //# sourceMappingURL=recipes.js.map