@idealyst/mcp-server 1.2.19 → 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.
@@ -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