expo-bbase 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2520,267 +2520,594 @@ var flashlist_default = flashlistModule;
2520
2520
  var uiReusablesModule = {
2521
2521
  id: "ui-reusables",
2522
2522
  name: "reactnative.reusables UI",
2523
- description: "\u9884\u7F6E UI \u7EC4\u4EF6",
2523
+ description: "\u9884\u7F6E UI \u7EC4\u4EF6 (Button, AlertDialog, Card, Input, Label, Text, Separator)",
2524
2524
  defaultChecked: false,
2525
2525
  dependencies: {
2526
- "reactnative.reusables": "^0.1.0",
2527
- "react-native-svg": "^15.8.0",
2526
+ "class-variance-authority": "^0.7.1",
2527
+ "clsx": "^2.1.1",
2528
+ "tailwind-merge": "^2.6.0",
2528
2529
  "@rn-primitives/slot": "^1.1.0",
2529
- "@rn-primitives/types": "^1.1.0"
2530
+ "@rn-primitives/types": "^1.1.0",
2531
+ "@rn-primitives/label": "^1.1.0",
2532
+ "@rn-primitives/separator": "^1.1.0",
2533
+ "@rn-primitives/alert-dialog": "^1.1.0",
2534
+ "react-native-svg": "^15.8.0"
2530
2535
  },
2531
2536
  devDependencies: {},
2532
2537
  files: [
2538
+ // ─── lib/utils.ts ────────────────────────────────────────────────────
2533
2539
  {
2534
- path: "src/components/ui/button.tsx",
2540
+ path: "src/lib/utils.ts",
2535
2541
  content: lines(
2536
- 'import React from "react";',
2537
- 'import { Text, Pressable, type PressableProps, type StyleProp, type ViewStyle, type TextStyle } from "react-native";',
2538
- 'import { Slot } from "@rn-primitives/slot";',
2539
- "",
2540
- 'type ButtonVariant = "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";',
2541
- 'type ButtonSize = "default" | "sm" | "lg" | "icon";',
2542
+ 'import { type ClassValue, clsx } from "clsx";',
2543
+ 'import { twMerge } from "tailwind-merge";',
2542
2544
  "",
2543
- "interface ButtonProps extends PressableProps {",
2544
- " variant?: ButtonVariant;",
2545
- " size?: ButtonSize;",
2546
- " asChild?: boolean;",
2547
- " style?: StyleProp<ViewStyle>;",
2548
- " textStyle?: StyleProp<TextStyle>;",
2549
- " children: React.ReactNode;",
2545
+ "export function cn(...inputs: ClassValue[]) {",
2546
+ " return twMerge(clsx(inputs));",
2550
2547
  "}",
2548
+ ""
2549
+ )
2550
+ },
2551
+ // ─── components/ui/text.tsx ──────────────────────────────────────────
2552
+ {
2553
+ path: "src/components/ui/text.tsx",
2554
+ content: lines(
2555
+ 'import { cn } from "@/src/lib/utils";',
2556
+ 'import { Slot } from "@rn-primitives/slot";',
2557
+ 'import { cva, type VariantProps } from "class-variance-authority";',
2558
+ 'import * as React from "react";',
2559
+ 'import { Platform, Text as RNText, type Role } from "react-native";',
2560
+ "",
2561
+ "const textVariants = cva(",
2562
+ " cn(",
2563
+ ' "text-foreground text-base",',
2564
+ " Platform.select({",
2565
+ ' web: "select-text",',
2566
+ " })",
2567
+ " ),",
2568
+ " {",
2569
+ " variants: {",
2570
+ " variant: {",
2571
+ ' default: "",',
2572
+ " h1: cn(",
2573
+ ' "text-center text-4xl font-extrabold tracking-tight",',
2574
+ ' Platform.select({ web: "scroll-m-20 text-balance" })',
2575
+ " ),",
2576
+ " h2: cn(",
2577
+ ' "border-border border-b pb-2 text-3xl font-semibold tracking-tight",',
2578
+ ' Platform.select({ web: "scroll-m-20 first:mt-0" })',
2579
+ " ),",
2580
+ " h3: cn(",
2581
+ ' "text-2xl font-semibold tracking-tight",',
2582
+ ' Platform.select({ web: "scroll-m-20" })',
2583
+ " ),",
2584
+ " h4: cn(",
2585
+ ' "text-xl font-semibold tracking-tight",',
2586
+ ' Platform.select({ web: "scroll-m-20" })',
2587
+ " ),",
2588
+ ' p: "mt-3 leading-7 sm:mt-6",',
2589
+ ' blockquote: "mt-4 border-l-2 pl-3 italic sm:mt-6 sm:pl-6",',
2590
+ " code: cn(",
2591
+ ' "bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold"',
2592
+ " ),",
2593
+ ' lead: "text-muted-foreground text-xl",',
2594
+ ' large: "text-lg font-semibold",',
2595
+ ' small: "text-sm font-medium leading-none",',
2596
+ ' muted: "text-muted-foreground text-sm",',
2597
+ " },",
2598
+ " },",
2599
+ " defaultVariants: {",
2600
+ ' variant: "default",',
2601
+ " },",
2602
+ " }",
2603
+ ");",
2551
2604
  "",
2552
- "const variantStyles: Record<ButtonVariant, ViewStyle> = {",
2553
- ' default: { backgroundColor: "#0f172a" },',
2554
- ' destructive: { backgroundColor: "#ef4444" },',
2555
- ' outline: { backgroundColor: "transparent", borderWidth: 1, borderColor: "#d4d4d8" },',
2556
- ' secondary: { backgroundColor: "#f4f4f5" },',
2557
- ' ghost: { backgroundColor: "transparent" },',
2558
- ' link: { backgroundColor: "transparent" },',
2559
- "};",
2605
+ "type TextVariantProps = VariantProps<typeof textVariants>;",
2560
2606
  "",
2561
- "const variantTextStyles: Record<ButtonVariant, TextStyle> = {",
2562
- ' default: { color: "#fafafa" },',
2563
- ' destructive: { color: "#fafafa" },',
2564
- ' outline: { color: "#18181b" },',
2565
- ' secondary: { color: "#18181b" },',
2566
- ' ghost: { color: "#18181b" },',
2567
- ' link: { color: "#2563eb", textDecorationLine: "underline" },',
2568
- "};",
2607
+ 'type TextVariant = NonNullable<TextVariantProps["variant"]>;',
2569
2608
  "",
2570
- "const sizeStyles: Record<ButtonSize, ViewStyle> = {",
2571
- " default: { height: 44, paddingHorizontal: 16 },",
2572
- " sm: { height: 36, paddingHorizontal: 12 },",
2573
- " lg: { height: 52, paddingHorizontal: 24 },",
2574
- " icon: { height: 44, width: 44 },",
2609
+ "const ROLE: Partial<Record<TextVariant, Role>> = {",
2610
+ ' h1: "heading",',
2611
+ ' h2: "heading",',
2612
+ ' h3: "heading",',
2613
+ ' h4: "heading",',
2614
+ ' blockquote: Platform.select({ web: "blockquote" as Role }),',
2615
+ ' code: Platform.select({ web: "code" as Role }),',
2575
2616
  "};",
2576
2617
  "",
2577
- "const sizeTextStyles: Record<ButtonSize, TextStyle> = {",
2578
- " default: { fontSize: 16 },",
2579
- " sm: { fontSize: 14 },",
2580
- " lg: { fontSize: 18 },",
2581
- " icon: { fontSize: 16 },",
2618
+ "const ARIA_LEVEL: Partial<Record<TextVariant, string>> = {",
2619
+ ' h1: "1",',
2620
+ ' h2: "2",',
2621
+ ' h3: "3",',
2622
+ ' h4: "4",',
2582
2623
  "};",
2583
2624
  "",
2584
- "export function Button({",
2585
- ' variant = "default",',
2586
- ' size = "default",',
2625
+ "const TextClassContext = React.createContext<string | undefined>(undefined);",
2626
+ "",
2627
+ "function Text({",
2628
+ " className,",
2587
2629
  " asChild = false,",
2588
- " style,",
2589
- " textStyle,",
2590
- " children,",
2591
- " ...rest",
2592
- "}: ButtonProps) {",
2593
- " const containerStyle: StyleProp<ViewStyle> = [",
2594
- " {",
2595
- " borderRadius: 8,",
2596
- ' flexDirection: "row",',
2597
- ' alignItems: "center",',
2598
- ' justifyContent: "center",',
2599
- " },",
2600
- " variantStyles[variant],",
2601
- " sizeStyles[size],",
2602
- " style,",
2603
- " ];",
2630
+ ' variant = "default",',
2631
+ " ...props",
2632
+ "}: React.ComponentProps<typeof RNText> &",
2633
+ " React.RefAttributes<typeof RNText> &",
2634
+ " TextVariantProps & { asChild?: boolean }) {",
2635
+ " const textClass = React.useContext(TextClassContext);",
2636
+ " const Component = asChild ? Slot : RNText;",
2637
+ " return (",
2638
+ " <Component",
2639
+ " className={cn(textVariants({ variant }), textClass, className)}",
2640
+ " role={variant ? ROLE[variant] : undefined}",
2641
+ " aria-level={variant ? ARIA_LEVEL[variant] : undefined}",
2642
+ " {...props}",
2643
+ " />",
2644
+ " );",
2645
+ "}",
2604
2646
  "",
2605
- " const textStyling: StyleProp<TextStyle> = [",
2606
- " {",
2607
- ' fontWeight: "600",',
2608
- ' textAlign: "center",',
2647
+ "export { Text, TextClassContext };",
2648
+ ""
2649
+ )
2650
+ },
2651
+ // ─── components/ui/button.tsx ────────────────────────────────────────
2652
+ {
2653
+ path: "src/components/ui/button.tsx",
2654
+ content: lines(
2655
+ 'import { TextClassContext } from "@/src/components/ui/text";',
2656
+ 'import { cn } from "@/src/lib/utils";',
2657
+ 'import { cva, type VariantProps } from "class-variance-authority";',
2658
+ 'import { Platform, Pressable } from "react-native";',
2659
+ "",
2660
+ "const buttonVariants = cva(",
2661
+ " cn(",
2662
+ ' "group shrink-0 flex-row items-center justify-center gap-2 rounded-md shadow-none",',
2663
+ " Platform.select({",
2664
+ ` web: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",`,
2665
+ " })",
2666
+ " ),",
2667
+ " {",
2668
+ " variants: {",
2669
+ " variant: {",
2670
+ " default: cn(",
2671
+ ' "bg-primary active:bg-primary/90 shadow-sm shadow-black/5",',
2672
+ ' Platform.select({ web: "hover:bg-primary/90" })',
2673
+ " ),",
2674
+ " destructive: cn(",
2675
+ ' "bg-destructive active:bg-destructive/90 dark:bg-destructive/60 shadow-sm shadow-black/5",',
2676
+ " Platform.select({",
2677
+ ' web: "hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",',
2678
+ " })",
2679
+ " ),",
2680
+ " outline: cn(",
2681
+ ' "border-border bg-background active:bg-accent dark:bg-input/30 dark:border-input dark:active:bg-input/50 border shadow-sm shadow-black/5",',
2682
+ " Platform.select({",
2683
+ ' web: "hover:bg-accent dark:hover:bg-input/50",',
2684
+ " })",
2685
+ " ),",
2686
+ " secondary: cn(",
2687
+ ' "bg-secondary active:bg-secondary/80 shadow-sm shadow-black/5",',
2688
+ ' Platform.select({ web: "hover:bg-secondary/80" })',
2689
+ " ),",
2690
+ " ghost: cn(",
2691
+ ' "active:bg-accent dark:active:bg-accent/50",',
2692
+ ' Platform.select({ web: "hover:bg-accent dark:hover:bg-accent/50" })',
2693
+ " ),",
2694
+ ' link: "",',
2695
+ " },",
2696
+ " size: {",
2697
+ " default: cn(",
2698
+ ' "h-10 px-4 py-2 sm:h-9",',
2699
+ ' Platform.select({ web: "has-[>svg]:px-3" })',
2700
+ " ),",
2701
+ " sm: cn(",
2702
+ ' "h-9 gap-1.5 rounded-md px-3 sm:h-8",',
2703
+ ' Platform.select({ web: "has-[>svg]:px-2.5" })',
2704
+ " ),",
2705
+ " lg: cn(",
2706
+ ' "h-11 rounded-md px-6 sm:h-10",',
2707
+ ' Platform.select({ web: "has-[>svg]:px-4" })',
2708
+ " ),",
2709
+ ' icon: "h-10 w-10 sm:h-9 sm:w-9",',
2710
+ " },",
2711
+ " },",
2712
+ " defaultVariants: {",
2713
+ ' variant: "default",',
2714
+ ' size: "default",',
2609
2715
  " },",
2610
- " variantTextStyles[variant],",
2611
- " sizeTextStyles[size],",
2612
- " textStyle,",
2613
- " ];",
2716
+ " }",
2717
+ ");",
2614
2718
  "",
2615
- " if (asChild && React.isValidElement(children)) {",
2616
- " return (",
2617
- " <Pressable style={containerStyle} {...rest}>",
2618
- " <Slot>{children}</Slot>",
2619
- " </Pressable>",
2620
- " );",
2719
+ "const buttonTextVariants = cva(",
2720
+ " cn(",
2721
+ ' "text-foreground text-sm font-medium",',
2722
+ ' Platform.select({ web: "pointer-events-none transition-colors" })',
2723
+ " ),",
2724
+ " {",
2725
+ " variants: {",
2726
+ " variant: {",
2727
+ ' default: "text-primary-foreground",',
2728
+ ' destructive: "text-white",',
2729
+ " outline: cn(",
2730
+ ' "group-active:text-accent-foreground",',
2731
+ ' Platform.select({ web: "group-hover:text-accent-foreground" })',
2732
+ " ),",
2733
+ ' secondary: "text-secondary-foreground",',
2734
+ ' ghost: "group-active:text-accent-foreground",',
2735
+ " link: cn(",
2736
+ ' "text-primary group-active:underline",',
2737
+ ' Platform.select({ web: "underline-offset-4 hover:underline group-hover:underline" })',
2738
+ " ),",
2739
+ " },",
2740
+ " size: {",
2741
+ ' default: "",',
2742
+ ' sm: "",',
2743
+ ' lg: "",',
2744
+ ' icon: "",',
2745
+ " },",
2746
+ " },",
2747
+ " defaultVariants: {",
2748
+ ' variant: "default",',
2749
+ ' size: "default",',
2750
+ " },",
2621
2751
  " }",
2752
+ ");",
2753
+ "",
2754
+ "type ButtonProps = React.ComponentProps<typeof Pressable> &",
2755
+ " React.RefAttributes<typeof Pressable> &",
2756
+ " VariantProps<typeof buttonVariants>;",
2622
2757
  "",
2758
+ "function Button({ className, variant, size, ...props }: ButtonProps) {",
2623
2759
  " return (",
2624
- " <Pressable style={containerStyle} {...rest}>",
2625
- ' {typeof children === "string" ? (',
2626
- " <Text style={textStyling}>{children}</Text>",
2627
- " ) : (",
2628
- " children",
2629
- " )}",
2630
- " </Pressable>",
2760
+ " <TextClassContext.Provider value={buttonTextVariants({ variant, size })}>",
2761
+ " <Pressable",
2762
+ ' className={cn(props.disabled && "opacity-50", buttonVariants({ variant, size }), className)}',
2763
+ ' role="button"',
2764
+ " {...props}",
2765
+ " />",
2766
+ " </TextClassContext.Provider>",
2631
2767
  " );",
2632
2768
  "}",
2633
2769
  "",
2634
- "export default Button;"
2770
+ "export { Button, buttonTextVariants, buttonVariants };",
2771
+ "export type { ButtonProps };",
2772
+ ""
2635
2773
  )
2636
2774
  },
2775
+ // ─── components/ui/input.tsx ──────────────────────────────────────────
2637
2776
  {
2638
2777
  path: "src/components/ui/input.tsx",
2639
2778
  content: lines(
2640
- 'import React, { forwardRef } from "react";',
2641
- "import {",
2642
- " TextInput,",
2643
- " type TextInputProps,",
2644
- " type StyleProp,",
2645
- " type ViewStyle,",
2646
- " type TextStyle,",
2647
- " View,",
2648
- " Text,",
2649
- '} from "react-native";',
2650
- "",
2651
- "interface InputProps extends TextInputProps {",
2652
- " label?: string;",
2653
- " error?: string;",
2654
- " containerStyle?: StyleProp<ViewStyle>;",
2655
- " inputStyle?: StyleProp<TextStyle>;",
2656
- "}",
2657
- "",
2658
- "export const Input = forwardRef<TextInput, InputProps>(",
2659
- " ({ label, error, containerStyle, inputStyle, ...rest }, ref) => {",
2660
- " return (",
2661
- " <View style={containerStyle}>",
2662
- " {label && <Text style={styles.label}>{label}</Text>}",
2663
- " <TextInput",
2664
- " ref={ref}",
2665
- " style={[",
2666
- " styles.input,",
2667
- " error && styles.inputError,",
2668
- " inputStyle,",
2669
- " ]}",
2670
- ' placeholderTextColor="#a1a1aa"',
2671
- " {...rest}",
2672
- " />",
2673
- " {error && <Text style={styles.errorText}>{error}</Text>}",
2674
- " </View>",
2675
- " );",
2676
- " }",
2677
- ");",
2779
+ 'import { cn } from "@/src/lib/utils";',
2780
+ 'import { Platform, TextInput } from "react-native";',
2781
+ "",
2782
+ "function Input({",
2783
+ " className,",
2784
+ " ...props",
2785
+ "}: React.ComponentProps<typeof TextInput> & React.RefAttributes<TextInput>) {",
2786
+ " return (",
2787
+ " <TextInput",
2788
+ " className={cn(",
2789
+ ' "dark:bg-input/30 border-input bg-background text-foreground flex h-10 w-full min-w-0 flex-row items-center rounded-md border px-3 py-1 text-base leading-5 shadow-sm shadow-black/5 sm:h-9",',
2790
+ " props.editable === false &&",
2791
+ " cn(",
2792
+ ' "opacity-50",',
2793
+ ' Platform.select({ web: "disabled:pointer-events-none disabled:cursor-not-allowed" })',
2794
+ " ),",
2795
+ " Platform.select({",
2796
+ " web: cn(",
2797
+ ' "placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground outline-none transition-[color,box-shadow] md:text-sm",',
2798
+ ' "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",',
2799
+ ' "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"',
2800
+ " ),",
2801
+ ' native: "placeholder:text-muted-foreground/50",',
2802
+ " }),",
2803
+ " className",
2804
+ " )}",
2805
+ " {...props}",
2806
+ " />",
2807
+ " );",
2808
+ "}",
2678
2809
  "",
2679
- 'Input.displayName = "Input";',
2810
+ "export { Input };",
2811
+ ""
2812
+ )
2813
+ },
2814
+ // ─── components/ui/label.tsx ─────────────────────────────────────────
2815
+ {
2816
+ path: "src/components/ui/label.tsx",
2817
+ content: lines(
2818
+ 'import { cn } from "@/src/lib/utils";',
2819
+ 'import * as LabelPrimitive from "@rn-primitives/label";',
2820
+ 'import { Platform } from "react-native";',
2680
2821
  "",
2681
- "const styles = {",
2682
- " label: {",
2683
- " fontSize: 14,",
2684
- ' fontWeight: "500",',
2685
- ' color: "#18181b",',
2686
- " marginBottom: 6,",
2687
- " } as TextStyle,",
2688
- " input: {",
2689
- " height: 44,",
2690
- " borderWidth: 1,",
2691
- ' borderColor: "#d4d4d8",',
2692
- " borderRadius: 8,",
2693
- " paddingHorizontal: 12,",
2694
- " fontSize: 16,",
2695
- ' color: "#18181b",',
2696
- ' backgroundColor: "#ffffff",',
2697
- " } as TextStyle,",
2698
- " inputError: {",
2699
- ' borderColor: "#ef4444",',
2700
- " } as TextStyle,",
2701
- " errorText: {",
2702
- " fontSize: 12,",
2703
- ' color: "#ef4444",',
2704
- " marginTop: 4,",
2705
- " } as TextStyle,",
2706
- "};",
2822
+ "function Label({",
2823
+ " className,",
2824
+ " onPress,",
2825
+ " onLongPress,",
2826
+ " onPressIn,",
2827
+ " onPressOut,",
2828
+ " disabled,",
2829
+ " ...props",
2830
+ "}: React.ComponentProps<typeof LabelPrimitive.Text>) {",
2831
+ " return (",
2832
+ " <LabelPrimitive.Root",
2833
+ " className={cn(",
2834
+ ' "flex select-none flex-row items-center gap-2",',
2835
+ " Platform.select({",
2836
+ ' web: "cursor-default leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",',
2837
+ " }),",
2838
+ ' disabled && "opacity-50"',
2839
+ " )}",
2840
+ " onPress={onPress}",
2841
+ " onLongPress={onLongPress}",
2842
+ " onPressIn={onPressIn}",
2843
+ " onPressOut={onPressOut}",
2844
+ " disabled={disabled}",
2845
+ " >",
2846
+ " <LabelPrimitive.Text",
2847
+ " className={cn(",
2848
+ ' "text-foreground text-sm font-medium",',
2849
+ ' Platform.select({ web: "leading-none" }),',
2850
+ " className",
2851
+ " )}",
2852
+ " {...props}",
2853
+ " />",
2854
+ " </LabelPrimitive.Root>",
2855
+ " );",
2856
+ "}",
2707
2857
  "",
2708
- "export default Input;"
2858
+ "export { Label };",
2859
+ ""
2709
2860
  )
2710
2861
  },
2862
+ // ─── components/ui/card.tsx ──────────────────────────────────────────
2711
2863
  {
2712
2864
  path: "src/components/ui/card.tsx",
2713
2865
  content: lines(
2714
- 'import React from "react";',
2715
- 'import { View, Text, type StyleProp, type ViewStyle, type TextStyle } from "react-native";',
2866
+ 'import { Text, TextClassContext } from "@/src/components/ui/text";',
2867
+ 'import { cn } from "@/src/lib/utils";',
2868
+ 'import { View } from "react-native";',
2869
+ "",
2870
+ "function Card({",
2871
+ " className,",
2872
+ " ...props",
2873
+ "}: React.ComponentProps<typeof View> & React.RefAttributes<View>) {",
2874
+ " return (",
2875
+ ' <TextClassContext.Provider value="text-card-foreground">',
2876
+ " <View",
2877
+ " className={cn(",
2878
+ ' "bg-card border-border flex flex-col gap-6 rounded-xl border py-6 shadow-sm shadow-black/5",',
2879
+ " className",
2880
+ " )}",
2881
+ " {...props}",
2882
+ " />",
2883
+ " </TextClassContext.Provider>",
2884
+ " );",
2885
+ "}",
2716
2886
  "",
2717
- "interface CardProps {",
2718
- " title?: string;",
2719
- " description?: string;",
2720
- " children: React.ReactNode;",
2721
- " style?: StyleProp<ViewStyle>;",
2722
- " titleStyle?: StyleProp<TextStyle>;",
2723
- " descriptionStyle?: StyleProp<TextStyle>;",
2724
- ' padding?: "none" | "sm" | "md" | "lg";',
2887
+ "function CardHeader({",
2888
+ " className,",
2889
+ " ...props",
2890
+ "}: React.ComponentProps<typeof View> & React.RefAttributes<View>) {",
2891
+ " return (",
2892
+ ' <View className={cn("flex flex-col gap-1.5 px-6", className)} {...props} />',
2893
+ " );",
2725
2894
  "}",
2726
2895
  "",
2727
- 'const paddingMap: Record<NonNullable<CardProps["padding"]>, number> = {',
2728
- " none: 0,",
2729
- " sm: 8,",
2730
- " md: 16,",
2731
- " lg: 24,",
2732
- "};",
2896
+ "function CardTitle({",
2897
+ " className,",
2898
+ " ref,",
2899
+ " ...props",
2900
+ "}: React.ComponentProps<typeof Text> & React.RefAttributes<typeof Text>) {",
2901
+ " return (",
2902
+ " <Text",
2903
+ " ref={ref}",
2904
+ ' role="heading"',
2905
+ " aria-level={3}",
2906
+ ' className={cn("font-semibold leading-none", className)}',
2907
+ " {...props}",
2908
+ " />",
2909
+ " );",
2910
+ "}",
2733
2911
  "",
2734
- "export function Card({",
2735
- " title,",
2736
- " description,",
2737
- " children,",
2738
- " style,",
2739
- " titleStyle,",
2740
- " descriptionStyle,",
2741
- ' padding = "md",',
2742
- "}: CardProps) {",
2912
+ "function CardDescription({",
2913
+ " className,",
2914
+ " ...props",
2915
+ "}: React.ComponentProps<typeof Text> & React.RefAttributes<typeof Text>) {",
2743
2916
  " return (",
2744
- " <View",
2745
- " style={[",
2746
- " {",
2747
- ' backgroundColor: "#ffffff",',
2748
- " borderRadius: 12,",
2749
- " borderWidth: 1,",
2750
- ' borderColor: "#e4e4e7",',
2751
- " padding: paddingMap[padding],",
2752
- " },",
2753
- " style,",
2754
- " ]}",
2755
- " >",
2756
- " {title && (",
2757
- " <Text style={[styles.title, titleStyle]}>{title}</Text>",
2917
+ ' <Text className={cn("text-muted-foreground text-sm", className)} {...props} />',
2918
+ " );",
2919
+ "}",
2920
+ "",
2921
+ "function CardContent({",
2922
+ " className,",
2923
+ " ...props",
2924
+ "}: React.ComponentProps<typeof View> & React.RefAttributes<View>) {",
2925
+ " return (",
2926
+ ' <View className={cn("px-6", className)} {...props} />',
2927
+ " );",
2928
+ "}",
2929
+ "",
2930
+ "function CardFooter({",
2931
+ " className,",
2932
+ " ...props",
2933
+ "}: React.ComponentProps<typeof View> & React.RefAttributes<View>) {",
2934
+ " return (",
2935
+ ' <View className={cn("flex flex-row items-center px-6", className)} {...props} />',
2936
+ " );",
2937
+ "}",
2938
+ "",
2939
+ "export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };",
2940
+ ""
2941
+ )
2942
+ },
2943
+ // ─── components/ui/separator.tsx ──────────────────────────────────────
2944
+ {
2945
+ path: "src/components/ui/separator.tsx",
2946
+ content: lines(
2947
+ 'import { cn } from "@/src/lib/utils";',
2948
+ 'import * as SeparatorPrimitive from "@rn-primitives/separator";',
2949
+ "",
2950
+ "function Separator({",
2951
+ " className,",
2952
+ ' orientation = "horizontal",',
2953
+ " decorative = true,",
2954
+ " ...props",
2955
+ "}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {",
2956
+ " return (",
2957
+ " <SeparatorPrimitive.Root",
2958
+ " decorative={decorative}",
2959
+ " orientation={orientation}",
2960
+ " className={cn(",
2961
+ ' "bg-border shrink-0",',
2962
+ ' orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",',
2963
+ " className",
2758
2964
  " )}",
2759
- " {description && (",
2760
- " <Text style={[styles.description, descriptionStyle]}>",
2761
- " {description}",
2762
- " </Text>",
2965
+ " {...props}",
2966
+ " />",
2967
+ " );",
2968
+ "}",
2969
+ "",
2970
+ "export { Separator };",
2971
+ ""
2972
+ )
2973
+ },
2974
+ // ─── components/ui/alert-dialog.tsx ──────────────────────────────────
2975
+ {
2976
+ path: "src/components/ui/alert-dialog.tsx",
2977
+ content: lines(
2978
+ 'import { buttonTextVariants, buttonVariants } from "@/src/components/ui/button";',
2979
+ 'import { TextClassContext } from "@/src/components/ui/text";',
2980
+ 'import { cn } from "@/src/lib/utils";',
2981
+ 'import * as AlertDialogPrimitive from "@rn-primitives/alert-dialog";',
2982
+ 'import * as React from "react";',
2983
+ 'import { Platform, View, type ViewProps } from "react-native";',
2984
+ "",
2985
+ "const AlertDialog = AlertDialogPrimitive.Root;",
2986
+ "",
2987
+ "const AlertDialogTrigger = AlertDialogPrimitive.Trigger;",
2988
+ "",
2989
+ "const AlertDialogPortal = AlertDialogPrimitive.Portal;",
2990
+ "",
2991
+ "function AlertDialogOverlay({",
2992
+ " className,",
2993
+ " ...props",
2994
+ '}: Omit<React.ComponentProps<typeof AlertDialogPrimitive.Overlay>, "asChild"> & {',
2995
+ " children?: React.ReactNode;",
2996
+ "}) {",
2997
+ " return (",
2998
+ " <AlertDialogPrimitive.Overlay",
2999
+ " className={cn(",
3000
+ ' "absolute bottom-0 left-0 right-0 top-0 z-50 flex items-center justify-center bg-black/50 p-2",',
3001
+ " Platform.select({",
3002
+ ' web: "animate-in fade-in-0 fixed",',
3003
+ " }),",
3004
+ " className",
2763
3005
  " )}",
2764
- " {children}",
2765
- " </View>",
3006
+ " {...props}",
3007
+ " />",
2766
3008
  " );",
2767
3009
  "}",
2768
3010
  "",
2769
- "const styles = {",
2770
- " title: {",
2771
- " fontSize: 18,",
2772
- ' fontWeight: "600",',
2773
- ' color: "#18181b",',
2774
- " marginBottom: 4,",
2775
- " } as TextStyle,",
2776
- " description: {",
2777
- " fontSize: 14,",
2778
- ' color: "#71717a",',
2779
- " marginBottom: 12,",
2780
- " } as TextStyle,",
2781
- "};",
3011
+ "function AlertDialogContent({",
3012
+ " className,",
3013
+ " portalHost,",
3014
+ " ...props",
3015
+ "}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {",
3016
+ " portalHost?: string;",
3017
+ "}) {",
3018
+ " return (",
3019
+ " <AlertDialogPortal hostName={portalHost}>",
3020
+ " <AlertDialogOverlay>",
3021
+ " <AlertDialogPrimitive.Content",
3022
+ " className={cn(",
3023
+ ' "bg-background border-border z-50 flex w-full max-w-[calc(100%-2rem)] flex-col gap-4 rounded-lg border p-6 shadow-lg shadow-black/5 sm:max-w-lg",',
3024
+ " Platform.select({",
3025
+ ' web: "animate-in fade-in-0 zoom-in-95 duration-200",',
3026
+ " }),",
3027
+ " className",
3028
+ " )}",
3029
+ " {...props}",
3030
+ " />",
3031
+ " </AlertDialogOverlay>",
3032
+ " </AlertDialogPortal>",
3033
+ " );",
3034
+ "}",
2782
3035
  "",
2783
- "export default Card;"
3036
+ "function AlertDialogHeader({ className, ...props }: ViewProps) {",
3037
+ " return (",
3038
+ ' <TextClassContext.Provider value="text-center sm:text-left">',
3039
+ ' <View className={cn("flex flex-col gap-2", className)} {...props} />',
3040
+ " </TextClassContext.Provider>",
3041
+ " );",
3042
+ "}",
3043
+ "",
3044
+ "function AlertDialogFooter({ className, ...props }: ViewProps) {",
3045
+ " return (",
3046
+ ' <View className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} {...props} />',
3047
+ " );",
3048
+ "}",
3049
+ "",
3050
+ "function AlertDialogTitle({",
3051
+ " className,",
3052
+ " ...props",
3053
+ "}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {",
3054
+ " return (",
3055
+ " <AlertDialogPrimitive.Title",
3056
+ ' className={cn("text-foreground text-lg font-semibold", className)}',
3057
+ " {...props}",
3058
+ " />",
3059
+ " );",
3060
+ "}",
3061
+ "",
3062
+ "function AlertDialogDescription({",
3063
+ " className,",
3064
+ " ...props",
3065
+ "}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {",
3066
+ " return (",
3067
+ " <AlertDialogPrimitive.Description",
3068
+ ' className={cn("text-muted-foreground text-sm", className)}',
3069
+ " {...props}",
3070
+ " />",
3071
+ " );",
3072
+ "}",
3073
+ "",
3074
+ "function AlertDialogAction({",
3075
+ " className,",
3076
+ " ...props",
3077
+ "}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {",
3078
+ " return (",
3079
+ " <TextClassContext.Provider value={buttonTextVariants({ className })}>",
3080
+ " <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />",
3081
+ " </TextClassContext.Provider>",
3082
+ " );",
3083
+ "}",
3084
+ "",
3085
+ "function AlertDialogCancel({",
3086
+ " className,",
3087
+ " ...props",
3088
+ "}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {",
3089
+ " return (",
3090
+ ' <TextClassContext.Provider value={buttonTextVariants({ className, variant: "outline" })}>',
3091
+ " <AlertDialogPrimitive.Cancel",
3092
+ ' className={cn(buttonVariants({ variant: "outline" }), className)} {...props} />',
3093
+ " </TextClassContext.Provider>",
3094
+ " );",
3095
+ "}",
3096
+ "",
3097
+ "export {",
3098
+ " AlertDialog,",
3099
+ " AlertDialogAction,",
3100
+ " AlertDialogCancel,",
3101
+ " AlertDialogContent,",
3102
+ " AlertDialogDescription,",
3103
+ " AlertDialogFooter,",
3104
+ " AlertDialogHeader,",
3105
+ " AlertDialogOverlay,",
3106
+ " AlertDialogPortal,",
3107
+ " AlertDialogTitle,",
3108
+ " AlertDialogTrigger,",
3109
+ "};",
3110
+ ""
2784
3111
  )
2785
3112
  }
2786
3113
  ]
@@ -2826,7 +3153,8 @@ function generateBaseTemplates(projectName) {
2826
3153
  // ─── app/_layout.tsx ────────────────────────────────────────────────
2827
3154
  {
2828
3155
  path: "app/_layout.tsx",
2829
- content: `import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
3156
+ content: `import "../global.css";
3157
+ import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
2830
3158
  import { useFonts } from "expo-font";
2831
3159
  import { Stack } from "expo-router";
2832
3160
  import * as SplashScreen from "expo-splash-screen";
@@ -3187,10 +3515,8 @@ export {};
3187
3515
  "slug": "${projectName}",
3188
3516
  "version": "1.0.0",
3189
3517
  "orientation": "portrait",
3190
- "icon": "./assets/icon.png",
3191
3518
  "userInterfaceStyle": "light",
3192
3519
  "splash": {
3193
- "image": "./assets/splash.png",
3194
3520
  "resizeMode": "contain",
3195
3521
  "backgroundColor": "#ffffff"
3196
3522
  },
@@ -3199,15 +3525,8 @@ export {};
3199
3525
  "bundleIdentifier": "com.${projectName}.app"
3200
3526
  },
3201
3527
  "android": {
3202
- "adaptiveIcon": {
3203
- "foregroundImage": "./assets/adaptive-icon.png",
3204
- "backgroundColor": "#ffffff"
3205
- },
3206
3528
  "package": "com.${projectName}.app"
3207
3529
  },
3208
- "web": {
3209
- "favicon": "./assets/favicon.png"
3210
- },
3211
3530
  "plugins": [
3212
3531
  "expo-router",
3213
3532
  "expo-splash-screen"
@@ -3251,8 +3570,45 @@ module.exports = {
3251
3570
  "./app/**/*.{js,jsx,ts,tsx}",
3252
3571
  "./src/**/*.{js,jsx,ts,tsx}",
3253
3572
  ],
3573
+ presets: [require("nativewind/preset")],
3254
3574
  theme: {
3255
- extend: {},
3575
+ extend: {
3576
+ colors: {
3577
+ border: "hsl(var(--border))",
3578
+ input: "hsl(var(--input))",
3579
+ ring: "hsl(var(--ring))",
3580
+ background: "hsl(var(--background))",
3581
+ foreground: "hsl(var(--foreground))",
3582
+ primary: {
3583
+ DEFAULT: "hsl(var(--primary))",
3584
+ foreground: "hsl(var(--primary-foreground))",
3585
+ },
3586
+ secondary: {
3587
+ DEFAULT: "hsl(var(--secondary))",
3588
+ foreground: "hsl(var(--secondary-foreground))",
3589
+ },
3590
+ destructive: {
3591
+ DEFAULT: "hsl(var(--destructive))",
3592
+ foreground: "hsl(var(--destructive-foreground))",
3593
+ },
3594
+ muted: {
3595
+ DEFAULT: "hsl(var(--muted))",
3596
+ foreground: "hsl(var(--muted-foreground))",
3597
+ },
3598
+ accent: {
3599
+ DEFAULT: "hsl(var(--accent))",
3600
+ foreground: "hsl(var(--accent-foreground))",
3601
+ },
3602
+ popover: {
3603
+ DEFAULT: "hsl(var(--popover))",
3604
+ foreground: "hsl(var(--popover-foreground))",
3605
+ },
3606
+ card: {
3607
+ DEFAULT: "hsl(var(--card))",
3608
+ foreground: "hsl(var(--card-foreground))",
3609
+ },
3610
+ },
3611
+ },
3256
3612
  },
3257
3613
  plugins: [],
3258
3614
  };
@@ -3262,10 +3618,11 @@ module.exports = {
3262
3618
  {
3263
3619
  path: "metro.config.js",
3264
3620
  content: `const { getDefaultConfig } = require("expo/metro-config");
3621
+ const { withNativeWind } = require("nativewind/metro");
3265
3622
 
3266
3623
  const config = getDefaultConfig(__dirname);
3267
3624
 
3268
- module.exports = config;
3625
+ module.exports = withNativeWind(config, { input: "./global.css" });
3269
3626
  `
3270
3627
  },
3271
3628
  // ─── babel.config.js ─────────────────────────────────────────────────
@@ -3274,10 +3631,67 @@ module.exports = config;
3274
3631
  content: `module.exports = function (api) {
3275
3632
  api.cache(true);
3276
3633
  return {
3277
- presets: ["babel-preset-expo"],
3278
- plugins: ["nativewind/babel"],
3634
+ presets: [
3635
+ ["babel-preset-expo", { jsxImportSource: "nativewind" }],
3636
+ "nativewind/babel",
3637
+ ],
3638
+ plugins: [],
3279
3639
  };
3280
3640
  };
3641
+ `
3642
+ },
3643
+ // ─── global.css (NativeWind CSS variables for rnr components) ─────────
3644
+ {
3645
+ path: "global.css",
3646
+ content: `@tailwind base;
3647
+ @tailwind components;
3648
+ @tailwind utilities;
3649
+
3650
+ @layer base {
3651
+ :root {
3652
+ --background: 0 0% 100%;
3653
+ --foreground: 240 10% 3.9%;
3654
+ --card: 0 0% 100%;
3655
+ --card-foreground: 240 10% 3.9%;
3656
+ --popover: 0 0% 100%;
3657
+ --popover-foreground: 240 10% 3.9%;
3658
+ --primary: 240 5.9% 10%;
3659
+ --primary-foreground: 0 0% 98%;
3660
+ --secondary: 240 4.8% 95.9%;
3661
+ --secondary-foreground: 240 5.9% 10%;
3662
+ --muted: 240 4.8% 95.9%;
3663
+ --muted-foreground: 240 3.8% 46.1%;
3664
+ --accent: 240 4.8% 95.9%;
3665
+ --accent-foreground: 240 5.9% 10%;
3666
+ --destructive: 0 84.2% 60.2%;
3667
+ --destructive-foreground: 0 0% 98%;
3668
+ --border: 240 5.9% 90%;
3669
+ --input: 240 5.9% 90%;
3670
+ --ring: 240 5.9% 10%;
3671
+ }
3672
+
3673
+ .dark {
3674
+ --background: 240 10% 3.9%;
3675
+ --foreground: 0 0% 98%;
3676
+ --card: 240 10% 3.9%;
3677
+ --card-foreground: 0 0% 98%;
3678
+ --popover: 240 10% 3.9%;
3679
+ --popover-foreground: 0 0% 98%;
3680
+ --primary: 0 0% 98%;
3681
+ --primary-foreground: 240 5.9% 10%;
3682
+ --secondary: 240 3.7% 15.9%;
3683
+ --secondary-foreground: 0 0% 98%;
3684
+ --muted: 240 3.7% 15.9%;
3685
+ --muted-foreground: 240 5% 64.9%;
3686
+ --accent: 240 3.7% 15.9%;
3687
+ --accent-foreground: 0 0% 98%;
3688
+ --destructive: 0 62.8% 30.6%;
3689
+ --destructive-foreground: 0 0% 98%;
3690
+ --border: 240 3.7% 15.9%;
3691
+ --input: 240 3.7% 15.9%;
3692
+ --ring: 240 4.9% 83.9%;
3693
+ }
3694
+ }
3281
3695
  `
3282
3696
  },
3283
3697
  // ─── .gitignore ─────────────────────────────────────────────────────
@@ -3326,6 +3740,392 @@ expo-env.d.ts
3326
3740
  ];
3327
3741
  }
3328
3742
 
3743
+ // src/templates/login-tabs.ts
3744
+ function generateLoginTabsTemplates(projectName) {
3745
+ return [
3746
+ // ─── app/_layout.tsx (overwrite base) ────────────────────────────────
3747
+ {
3748
+ path: "app/_layout.tsx",
3749
+ content: lines(
3750
+ 'import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";',
3751
+ 'import { useFonts } from "expo-font";',
3752
+ 'import { Stack } from "expo-router";',
3753
+ 'import * as SplashScreen from "expo-splash-screen";',
3754
+ 'import { useEffect } from "react";',
3755
+ 'import { useColorScheme } from "react-native";',
3756
+ "",
3757
+ 'import { Colors } from "@/src/constants/Colors";',
3758
+ "",
3759
+ "SplashScreen.preventAutoHideAsync();",
3760
+ "",
3761
+ "export default function RootLayout() {",
3762
+ " const colorScheme = useColorScheme();",
3763
+ " const [loaded] = useFonts({",
3764
+ ' SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),',
3765
+ " });",
3766
+ "",
3767
+ " useEffect(() => {",
3768
+ " if (loaded) {",
3769
+ " SplashScreen.hideAsync();",
3770
+ " }",
3771
+ " }, [loaded]);",
3772
+ "",
3773
+ " if (!loaded) {",
3774
+ " return null;",
3775
+ " }",
3776
+ "",
3777
+ " return (",
3778
+ ' <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>',
3779
+ " <Stack>",
3780
+ ' <Stack.Screen name="login" options={{ headerShown: false }} />',
3781
+ ' <Stack.Screen name="(tabs)" options={{ headerShown: false }} />',
3782
+ ' <Stack.Screen name="+not-found" />',
3783
+ " </Stack>",
3784
+ " </ThemeProvider>",
3785
+ " );",
3786
+ "}",
3787
+ ""
3788
+ )
3789
+ },
3790
+ // ─── app/login.tsx ───────────────────────────────────────────────────
3791
+ {
3792
+ path: "app/login.tsx",
3793
+ content: lines(
3794
+ 'import { SignInForm } from "@/src/components/SignInForm";',
3795
+ 'import { View } from "react-native";',
3796
+ "",
3797
+ "export default function LoginScreen() {",
3798
+ " return (",
3799
+ ' <View className="flex-1 items-center justify-center p-4">',
3800
+ " <SignInForm />",
3801
+ " </View>",
3802
+ " );",
3803
+ "}",
3804
+ ""
3805
+ )
3806
+ },
3807
+ // ─── app/(tabs)/_layout.tsx (overwrite base) ───────────────────────
3808
+ {
3809
+ path: "app/(tabs)/_layout.tsx",
3810
+ content: lines(
3811
+ 'import { Tabs } from "expo-router";',
3812
+ 'import { Platform } from "react-native";',
3813
+ "",
3814
+ 'import { Colors } from "@/src/constants/Colors";',
3815
+ 'import { useColorScheme } from "@/src/hooks/useColorScheme";',
3816
+ "",
3817
+ "export default function TabLayout() {",
3818
+ " const colorScheme = useColorScheme();",
3819
+ "",
3820
+ " return (",
3821
+ " <Tabs",
3822
+ " screenOptions={{",
3823
+ ' tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,',
3824
+ " headerStyle: {",
3825
+ ' backgroundColor: Colors[colorScheme ?? "light"].background,',
3826
+ " },",
3827
+ " headerShadowVisible: false,",
3828
+ " tabBarStyle: Platform.select({",
3829
+ " ios: {",
3830
+ ' position: "absolute",',
3831
+ " },",
3832
+ " default: {},",
3833
+ " }),",
3834
+ " }}",
3835
+ " >",
3836
+ " <Tabs.Screen",
3837
+ ' name="home"',
3838
+ " options={{",
3839
+ ' title: "Home",',
3840
+ " tabBarIcon: () => null,",
3841
+ " }}",
3842
+ " />",
3843
+ " <Tabs.Screen",
3844
+ ' name="list"',
3845
+ " options={{",
3846
+ ' title: "List",',
3847
+ " tabBarIcon: () => null,",
3848
+ " }}",
3849
+ " />",
3850
+ " <Tabs.Screen",
3851
+ ' name="mine"',
3852
+ " options={{",
3853
+ ' title: "Mine",',
3854
+ " tabBarIcon: () => null,",
3855
+ " }}",
3856
+ " />",
3857
+ " </Tabs>",
3858
+ " );",
3859
+ "}",
3860
+ ""
3861
+ )
3862
+ },
3863
+ // ─── app/(tabs)/home.tsx ────────────────────────────────────────────
3864
+ {
3865
+ path: "app/(tabs)/home.tsx",
3866
+ content: lines(
3867
+ 'import { Button } from "@/src/components/ui/button";',
3868
+ "import {",
3869
+ " AlertDialog,",
3870
+ " AlertDialogAction,",
3871
+ " AlertDialogCancel,",
3872
+ " AlertDialogContent,",
3873
+ " AlertDialogDescription,",
3874
+ " AlertDialogFooter,",
3875
+ " AlertDialogHeader,",
3876
+ " AlertDialogTitle,",
3877
+ " AlertDialogTrigger,",
3878
+ '} from "@/src/components/ui/alert-dialog";',
3879
+ 'import { Text } from "@/src/components/ui/text";',
3880
+ 'import { View } from "react-native";',
3881
+ "",
3882
+ "export default function HomeScreen() {",
3883
+ " return (",
3884
+ ' <View className="flex-1 gap-6 p-6">',
3885
+ ' <Text variant="h3">UI Components</Text>',
3886
+ ' <Text variant="muted">React Native Reusables components showcase</Text>',
3887
+ "",
3888
+ " {/* Button variants */}",
3889
+ ' <View className="gap-3">',
3890
+ ' <Text variant="large">Buttons</Text>',
3891
+ ' <View className="flex-row flex-wrap gap-2">',
3892
+ " <Button onPress={() => {}}>",
3893
+ " <Text>Default</Text>",
3894
+ " </Button>",
3895
+ ' <Button variant="secondary" onPress={() => {}}>',
3896
+ " <Text>Secondary</Text>",
3897
+ " </Button>",
3898
+ ' <Button variant="destructive" onPress={() => {}}>',
3899
+ " <Text>Destructive</Text>",
3900
+ " </Button>",
3901
+ ' <Button variant="outline" onPress={() => {}}>',
3902
+ " <Text>Outline</Text>",
3903
+ " </Button>",
3904
+ ' <Button variant="ghost" onPress={() => {}}>',
3905
+ " <Text>Ghost</Text>",
3906
+ " </Button>",
3907
+ ' <Button variant="link" onPress={() => {}}>',
3908
+ " <Text>Link</Text>",
3909
+ " </Button>",
3910
+ " </View>",
3911
+ " </View>",
3912
+ "",
3913
+ " {/* AlertDialog */}",
3914
+ ' <View className="gap-3">',
3915
+ ' <Text variant="large">Alert Dialog</Text>',
3916
+ " <AlertDialog>",
3917
+ " <AlertDialogTrigger asChild>",
3918
+ ' <Button variant="outline">',
3919
+ " <Text>Show Alert</Text>",
3920
+ " </Button>",
3921
+ " </AlertDialogTrigger>",
3922
+ " <AlertDialogContent>",
3923
+ " <AlertDialogHeader>",
3924
+ " <AlertDialogTitle>Are you sure?</AlertDialogTitle>",
3925
+ " <AlertDialogDescription>",
3926
+ " This action cannot be undone. This will permanently delete your account and remove your data from our servers.",
3927
+ " </AlertDialogDescription>",
3928
+ " </AlertDialogHeader>",
3929
+ " <AlertDialogFooter>",
3930
+ " <AlertDialogCancel>",
3931
+ " <Text>Cancel</Text>",
3932
+ " </AlertDialogCancel>",
3933
+ " <AlertDialogAction>",
3934
+ " <Text>Continue</Text>",
3935
+ " </AlertDialogAction>",
3936
+ " </AlertDialogFooter>",
3937
+ " </AlertDialogContent>",
3938
+ " </AlertDialog>",
3939
+ " </View>",
3940
+ " </View>",
3941
+ " );",
3942
+ "}",
3943
+ ""
3944
+ )
3945
+ },
3946
+ // ─── app/(tabs)/list.tsx ────────────────────────────────────────────
3947
+ {
3948
+ path: "app/(tabs)/list.tsx",
3949
+ content: lines(
3950
+ 'import { Text } from "@/src/components/ui/text";',
3951
+ 'import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/src/components/ui/card";',
3952
+ 'import { View, ScrollView } from "react-native";',
3953
+ "",
3954
+ "const ITEMS = Array.from({ length: 20 }, (_, i) => ({",
3955
+ " id: String(i + 1),",
3956
+ " title: `Item ${i + 1}`,",
3957
+ " description: `Description for item ${i + 1}`,",
3958
+ "}));",
3959
+ "",
3960
+ "export default function ListScreen() {",
3961
+ " return (",
3962
+ ' <ScrollView className="flex-1 p-4">',
3963
+ ' <Text variant="h3" className="mb-4">List</Text>',
3964
+ ' <View className="gap-3">',
3965
+ " {ITEMS.map((item) => (",
3966
+ " <Card key={item.id}>",
3967
+ " <CardHeader>",
3968
+ " <CardTitle>{item.title}</CardTitle>",
3969
+ " <CardDescription>{item.description}</CardDescription>",
3970
+ " </CardHeader>",
3971
+ " <CardContent />",
3972
+ " </Card>",
3973
+ " ))}",
3974
+ " </View>",
3975
+ " </ScrollView>",
3976
+ " );",
3977
+ "}",
3978
+ ""
3979
+ )
3980
+ },
3981
+ // ─── app/(tabs)/mine.tsx ────────────────────────────────────────────
3982
+ {
3983
+ path: "app/(tabs)/mine.tsx",
3984
+ content: lines(
3985
+ 'import { Button } from "@/src/components/ui/button";',
3986
+ 'import { Card, CardContent, CardHeader, CardTitle } from "@/src/components/ui/card";',
3987
+ 'import { Text } from "@/src/components/ui/text";',
3988
+ 'import { View } from "react-native";',
3989
+ 'import { router } from "expo-router";',
3990
+ "",
3991
+ "export default function MineScreen() {",
3992
+ " return (",
3993
+ ' <View className="flex-1 p-6">',
3994
+ ' <Text variant="h3" className="mb-6">Mine</Text>',
3995
+ "",
3996
+ ' <Card className="mb-6">',
3997
+ " <CardHeader>",
3998
+ " <CardTitle>Profile</CardTitle>",
3999
+ " </CardHeader>",
4000
+ " <CardContent>",
4001
+ ' <View className="gap-2">',
4002
+ ' <Text variant="muted">User Name</Text>',
4003
+ " <Text>user@example.com</Text>",
4004
+ " </View>",
4005
+ " </CardContent>",
4006
+ " </Card>",
4007
+ "",
4008
+ " <Button",
4009
+ ' variant="destructive"',
4010
+ ' className="w-full"',
4011
+ " onPress={() => {",
4012
+ ' router.replace("/login");',
4013
+ " }}",
4014
+ " >",
4015
+ " <Text>Sign Out</Text>",
4016
+ " </Button>",
4017
+ " </View>",
4018
+ " );",
4019
+ "}",
4020
+ ""
4021
+ )
4022
+ },
4023
+ // ─── src/components/SignInForm.tsx ───────────────────────────────────
4024
+ {
4025
+ path: "src/components/SignInForm.tsx",
4026
+ content: lines(
4027
+ 'import { Button } from "@/src/components/ui/button";',
4028
+ "import {",
4029
+ " Card,",
4030
+ " CardContent,",
4031
+ " CardDescription,",
4032
+ " CardHeader,",
4033
+ " CardTitle,",
4034
+ '} from "@/src/components/ui/card";',
4035
+ 'import { Input } from "@/src/components/ui/input";',
4036
+ 'import { Label } from "@/src/components/ui/label";',
4037
+ 'import { Separator } from "@/src/components/ui/separator";',
4038
+ 'import { Text } from "@/src/components/ui/text";',
4039
+ 'import * as React from "react";',
4040
+ 'import { Pressable, type TextInput, View } from "react-native";',
4041
+ 'import { router } from "expo-router";',
4042
+ "",
4043
+ "export function SignInForm() {",
4044
+ " const passwordInputRef = React.useRef<TextInput>(null);",
4045
+ "",
4046
+ " function onEmailSubmitEditing() {",
4047
+ " passwordInputRef.current?.focus();",
4048
+ " }",
4049
+ "",
4050
+ " function onSubmit() {",
4051
+ " // TODO: Submit form and navigate to protected screen if successful",
4052
+ ' router.replace("/(tabs)/home");',
4053
+ " }",
4054
+ "",
4055
+ " return (",
4056
+ ' <View className="gap-6 w-full max-w-sm">',
4057
+ ' <Card className="shadow-none sm:shadow-sm sm:shadow-black/5">',
4058
+ " <CardHeader>",
4059
+ ' <CardTitle className="text-center text-xl sm:text-left">Sign in to your app</CardTitle>',
4060
+ ' <CardDescription className="text-center sm:text-left">',
4061
+ " Welcome back! Please sign in to continue",
4062
+ " </CardDescription>",
4063
+ " </CardHeader>",
4064
+ ' <CardContent className="gap-6">',
4065
+ ' <View className="gap-6">',
4066
+ ' <View className="gap-1.5">',
4067
+ ' <Label nativeID="email">Email</Label>',
4068
+ " <Input",
4069
+ ' placeholder="m@example.com"',
4070
+ ' keyboardType="email-address"',
4071
+ ' autoComplete="email"',
4072
+ ' autoCapitalize="none"',
4073
+ " onSubmitEditing={onEmailSubmitEditing}",
4074
+ ' returnKeyType="next"',
4075
+ ' submitBehavior="submit"',
4076
+ " />",
4077
+ " </View>",
4078
+ ' <View className="gap-1.5">',
4079
+ ' <View className="flex-row items-center">',
4080
+ ' <Label nativeID="password">Password</Label>',
4081
+ " <Button",
4082
+ ' variant="link"',
4083
+ ' size="sm"',
4084
+ ' className="ml-auto h-4 px-1 py-0 sm:h-4"',
4085
+ " onPress={() => {",
4086
+ " // TODO: Navigate to forgot password screen",
4087
+ " }}",
4088
+ " >",
4089
+ ' <Text className="font-normal leading-4">Forgot your password?</Text>',
4090
+ " </Button>",
4091
+ " </View>",
4092
+ " <Input",
4093
+ " ref={passwordInputRef}",
4094
+ " secureTextEntry",
4095
+ ' returnKeyType="send"',
4096
+ " onSubmitEditing={onSubmit}",
4097
+ " />",
4098
+ " </View>",
4099
+ ' <Button className="w-full" onPress={onSubmit}>',
4100
+ " <Text>Continue</Text>",
4101
+ " </Button>",
4102
+ " </View>",
4103
+ ' <Text className="text-center text-sm">',
4104
+ ' Don&apos;t have an account?{" "}',
4105
+ " <Pressable",
4106
+ " onPress={() => {",
4107
+ " // TODO: Navigate to sign up screen",
4108
+ " }}",
4109
+ " >",
4110
+ ' <Text className="text-sm underline underline-offset-4">Sign up</Text>',
4111
+ " </Pressable>",
4112
+ " </Text>",
4113
+ ' <View className="flex-row items-center">',
4114
+ ' <Separator className="flex-1" />',
4115
+ ' <Text className="text-muted-foreground px-4 text-sm">or</Text>',
4116
+ ' <Separator className="flex-1" />',
4117
+ " </View>",
4118
+ " </CardContent>",
4119
+ " </Card>",
4120
+ " </View>",
4121
+ " );",
4122
+ "}",
4123
+ ""
4124
+ )
4125
+ }
4126
+ ];
4127
+ }
4128
+
3329
4129
  // src/utils/file.ts
3330
4130
  var import_fs_extra = __toESM(require("fs-extra"));
3331
4131
  var import_path = __toESM(require("path"));
@@ -3388,7 +4188,8 @@ function generateBasePackageJson(projectName) {
3388
4188
  "react-native-safe-area-context": "~5.6.0",
3389
4189
  "react-native-screens": "~4.16.0",
3390
4190
  nativewind: "^4.1.0",
3391
- tailwindcss: "^3.4.0"
4191
+ tailwindcss: "^3.4.0",
4192
+ "react-native-svg": "^15.8.0"
3392
4193
  },
3393
4194
  devDependencies: {
3394
4195
  "@types/react": "~19.1.0",
@@ -3399,7 +4200,7 @@ function generateBasePackageJson(projectName) {
3399
4200
 
3400
4201
  // src/index.ts
3401
4202
  var import_execa = require("execa");
3402
- var CLI_VERSION = "1.2.0";
4203
+ var CLI_VERSION = "1.3.1";
3403
4204
  var CONFIG_FILE = ".expo-bbase.json";
3404
4205
  async function run() {
3405
4206
  const program = new import_commander.Command();
@@ -3440,10 +4241,34 @@ async function createProject(projectName) {
3440
4241
  import_chalk.default.bold.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")
3441
4242
  );
3442
4243
  console.log();
4244
+ const { uiTemplate } = await (0, import_prompts.default)({
4245
+ type: "select",
4246
+ name: "uiTemplate",
4247
+ message: "Choose a UI template",
4248
+ choices: [
4249
+ {
4250
+ title: `${import_chalk.default.bold("Login + Tabs")} \u2014 Login page, Home/List/Mine tabs with rnr components`,
4251
+ value: "login-tabs",
4252
+ description: "Pre-built login form, 3-tab layout with Button & AlertDialog demos"
4253
+ },
4254
+ {
4255
+ title: `${import_chalk.default.bold("Default")} \u2014 Blank tabs (Home + Explore)`,
4256
+ value: "default",
4257
+ description: "Minimal starter with basic tab navigation"
4258
+ }
4259
+ ],
4260
+ initial: 0
4261
+ });
4262
+ if (uiTemplate === void 0) {
4263
+ console.log(import_chalk.default.yellow("\nCancelled."));
4264
+ process.exit(0);
4265
+ }
4266
+ const isLoginTabs = uiTemplate === "login-tabs";
3443
4267
  const choices = modules.map((m) => ({
3444
4268
  title: `${import_chalk.default.bold(m.name)} \u2014 ${import_chalk.default.gray(m.description)}`,
3445
4269
  value: m.id,
3446
- selected: m.defaultChecked
4270
+ // Auto-select ui-reusables when login-tabs template is chosen
4271
+ selected: isLoginTabs && m.id === "ui-reusables" ? true : m.defaultChecked
3447
4272
  }));
3448
4273
  const { selectedModules } = await (0, import_prompts.default)({
3449
4274
  type: "multiselect",
@@ -3457,11 +4282,20 @@ async function createProject(projectName) {
3457
4282
  console.log(import_chalk.default.yellow("\nCancelled."));
3458
4283
  process.exit(0);
3459
4284
  }
3460
- const selectedModuleDefs = getModulesByIds(selectedModules);
4285
+ let finalModuleIds = selectedModules;
4286
+ if (isLoginTabs && !finalModuleIds.includes("ui-reusables")) {
4287
+ finalModuleIds = ["ui-reusables", ...finalModuleIds];
4288
+ }
4289
+ const selectedModuleDefs = getModulesByIds(finalModuleIds);
3461
4290
  const targetDir = import_path2.default.resolve(process.cwd(), projectName);
3462
4291
  console.log();
3463
4292
  console.log(import_chalk.default.white(` \u{1F4E6} Project: ${import_chalk.default.bold(projectName)}`));
3464
4293
  console.log(import_chalk.default.white(` \u{1F4C2} Path: ${import_chalk.default.gray(targetDir)}`));
4294
+ console.log(
4295
+ import_chalk.default.white(
4296
+ ` \u{1F3A8} Template: ${import_chalk.default.green(isLoginTabs ? "Login + Tabs" : "Default")}`
4297
+ )
4298
+ );
3465
4299
  console.log(
3466
4300
  import_chalk.default.white(
3467
4301
  ` \u{1F9E9} Modules: ${import_chalk.default.green(selectedModuleDefs.map((m) => m.name).join(", ") || "none")}`
@@ -3484,6 +4318,15 @@ async function createProject(projectName) {
3484
4318
  await writeFile(filePath, content);
3485
4319
  }
3486
4320
  }
4321
+ if (isLoginTabs) {
4322
+ spinner.text = "Writing UI template files...";
4323
+ const loginTabsTemplates = generateLoginTabsTemplates(projectName);
4324
+ for (const file of loginTabsTemplates) {
4325
+ const filePath = import_path2.default.join(targetDir, file.path);
4326
+ const content = replaceTemplateVars(file.content, { projectName });
4327
+ await writeFile(filePath, content);
4328
+ }
4329
+ }
3487
4330
  spinner.text = "Generating package.json...";
3488
4331
  const pkgJson = generateBasePackageJson(projectName);
3489
4332
  const allDeps = {};
@@ -3507,8 +4350,9 @@ async function createProject(projectName) {
3507
4350
  await updateLayoutFile(targetDir, selectedModuleDefs);
3508
4351
  await writeProjectConfig(targetDir, {
3509
4352
  projectName,
3510
- selectedModules,
3511
- cliVersion: CLI_VERSION
4353
+ selectedModules: finalModuleIds,
4354
+ cliVersion: CLI_VERSION,
4355
+ uiTemplate: isLoginTabs ? "login-tabs" : "default"
3512
4356
  });
3513
4357
  spinner.text = "Installing dependencies (yarn install)...";
3514
4358
  try {
@@ -3527,6 +4371,9 @@ async function createProject(projectName) {
3527
4371
  console.log(import_chalk.default.bold(" \u{1F389} Next steps:"));
3528
4372
  console.log(import_chalk.default.white(` cd ${projectName}`));
3529
4373
  console.log(import_chalk.default.white(" npx expo start"));
4374
+ if (isLoginTabs) {
4375
+ console.log(import_chalk.default.gray(" \u2192 App starts at login page, sign in to see tabs"));
4376
+ }
3530
4377
  console.log();
3531
4378
  if (selectedModuleDefs.length > 0) {
3532
4379
  console.log(import_chalk.default.bold(" \u{1F4CB} Selected modules:"));