@idealyst/mcp-server 1.2.20 → 1.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data/navigation-guides.d.ts.map +1 -1
- package/dist/data/navigation-guides.js +445 -0
- package/dist/data/navigation-guides.js.map +1 -1
- package/dist/data/recipes.d.ts.map +1 -1
- package/dist/data/recipes.js +744 -0
- package/dist/data/recipes.js.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/examples/components/Card.examples.tsx +3 -4
- package/package.json +4 -4
package/dist/data/recipes.js
CHANGED
|
@@ -2161,6 +2161,750 @@ function ProfileScreen() {
|
|
|
2161
2161
|
],
|
|
2162
2162
|
relatedRecipes: ["form-with-validation"],
|
|
2163
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
|
+
},
|
|
2164
2908
|
};
|
|
2165
2909
|
/**
|
|
2166
2910
|
* Get all recipes grouped by category
|