expo-bbase 1.2.0 → 1.3.0

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
+ "}",
3035
+ "",
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
+ "}",
2782
3061
  "",
2783
- "export default Card;"
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";
@@ -3251,8 +3579,45 @@ module.exports = {
3251
3579
  "./app/**/*.{js,jsx,ts,tsx}",
3252
3580
  "./src/**/*.{js,jsx,ts,tsx}",
3253
3581
  ],
3582
+ presets: [require("nativewind/preset")],
3254
3583
  theme: {
3255
- extend: {},
3584
+ extend: {
3585
+ colors: {
3586
+ border: "hsl(var(--border))",
3587
+ input: "hsl(var(--input))",
3588
+ ring: "hsl(var(--ring))",
3589
+ background: "hsl(var(--background))",
3590
+ foreground: "hsl(var(--foreground))",
3591
+ primary: {
3592
+ DEFAULT: "hsl(var(--primary))",
3593
+ foreground: "hsl(var(--primary-foreground))",
3594
+ },
3595
+ secondary: {
3596
+ DEFAULT: "hsl(var(--secondary))",
3597
+ foreground: "hsl(var(--secondary-foreground))",
3598
+ },
3599
+ destructive: {
3600
+ DEFAULT: "hsl(var(--destructive))",
3601
+ foreground: "hsl(var(--destructive-foreground))",
3602
+ },
3603
+ muted: {
3604
+ DEFAULT: "hsl(var(--muted))",
3605
+ foreground: "hsl(var(--muted-foreground))",
3606
+ },
3607
+ accent: {
3608
+ DEFAULT: "hsl(var(--accent))",
3609
+ foreground: "hsl(var(--accent-foreground))",
3610
+ },
3611
+ popover: {
3612
+ DEFAULT: "hsl(var(--popover))",
3613
+ foreground: "hsl(var(--popover-foreground))",
3614
+ },
3615
+ card: {
3616
+ DEFAULT: "hsl(var(--card))",
3617
+ foreground: "hsl(var(--card-foreground))",
3618
+ },
3619
+ },
3620
+ },
3256
3621
  },
3257
3622
  plugins: [],
3258
3623
  };
@@ -3262,10 +3627,11 @@ module.exports = {
3262
3627
  {
3263
3628
  path: "metro.config.js",
3264
3629
  content: `const { getDefaultConfig } = require("expo/metro-config");
3630
+ const { withNativeWind } = require("nativewind/metro");
3265
3631
 
3266
3632
  const config = getDefaultConfig(__dirname);
3267
3633
 
3268
- module.exports = config;
3634
+ module.exports = withNativeWind(config, { input: "./global.css" });
3269
3635
  `
3270
3636
  },
3271
3637
  // ─── babel.config.js ─────────────────────────────────────────────────
@@ -3274,10 +3640,67 @@ module.exports = config;
3274
3640
  content: `module.exports = function (api) {
3275
3641
  api.cache(true);
3276
3642
  return {
3277
- presets: ["babel-preset-expo"],
3278
- plugins: ["nativewind/babel"],
3643
+ presets: [
3644
+ ["babel-preset-expo", { jsxImportSource: "nativewind" }],
3645
+ "nativewind/babel",
3646
+ ],
3647
+ plugins: ["react-native-reanimated/plugin"],
3279
3648
  };
3280
3649
  };
3650
+ `
3651
+ },
3652
+ // ─── global.css (NativeWind CSS variables for rnr components) ─────────
3653
+ {
3654
+ path: "global.css",
3655
+ content: `@tailwind base;
3656
+ @tailwind components;
3657
+ @tailwind utilities;
3658
+
3659
+ @layer base {
3660
+ :root {
3661
+ --background: 0 0% 100%;
3662
+ --foreground: 240 10% 3.9%;
3663
+ --card: 0 0% 100%;
3664
+ --card-foreground: 240 10% 3.9%;
3665
+ --popover: 0 0% 100%;
3666
+ --popover-foreground: 240 10% 3.9%;
3667
+ --primary: 240 5.9% 10%;
3668
+ --primary-foreground: 0 0% 98%;
3669
+ --secondary: 240 4.8% 95.9%;
3670
+ --secondary-foreground: 240 5.9% 10%;
3671
+ --muted: 240 4.8% 95.9%;
3672
+ --muted-foreground: 240 3.8% 46.1%;
3673
+ --accent: 240 4.8% 95.9%;
3674
+ --accent-foreground: 240 5.9% 10%;
3675
+ --destructive: 0 84.2% 60.2%;
3676
+ --destructive-foreground: 0 0% 98%;
3677
+ --border: 240 5.9% 90%;
3678
+ --input: 240 5.9% 90%;
3679
+ --ring: 240 5.9% 10%;
3680
+ }
3681
+
3682
+ .dark {
3683
+ --background: 240 10% 3.9%;
3684
+ --foreground: 0 0% 98%;
3685
+ --card: 240 10% 3.9%;
3686
+ --card-foreground: 0 0% 98%;
3687
+ --popover: 240 10% 3.9%;
3688
+ --popover-foreground: 0 0% 98%;
3689
+ --primary: 0 0% 98%;
3690
+ --primary-foreground: 240 5.9% 10%;
3691
+ --secondary: 240 3.7% 15.9%;
3692
+ --secondary-foreground: 0 0% 98%;
3693
+ --muted: 240 3.7% 15.9%;
3694
+ --muted-foreground: 240 5% 64.9%;
3695
+ --accent: 240 3.7% 15.9%;
3696
+ --accent-foreground: 0 0% 98%;
3697
+ --destructive: 0 62.8% 30.6%;
3698
+ --destructive-foreground: 0 0% 98%;
3699
+ --border: 240 3.7% 15.9%;
3700
+ --input: 240 3.7% 15.9%;
3701
+ --ring: 240 4.9% 83.9%;
3702
+ }
3703
+ }
3281
3704
  `
3282
3705
  },
3283
3706
  // ─── .gitignore ─────────────────────────────────────────────────────
@@ -3326,6 +3749,392 @@ expo-env.d.ts
3326
3749
  ];
3327
3750
  }
3328
3751
 
3752
+ // src/templates/login-tabs.ts
3753
+ function generateLoginTabsTemplates(projectName) {
3754
+ return [
3755
+ // ─── app/_layout.tsx (overwrite base) ────────────────────────────────
3756
+ {
3757
+ path: "app/_layout.tsx",
3758
+ content: lines(
3759
+ 'import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";',
3760
+ 'import { useFonts } from "expo-font";',
3761
+ 'import { Stack } from "expo-router";',
3762
+ 'import * as SplashScreen from "expo-splash-screen";',
3763
+ 'import { useEffect } from "react";',
3764
+ 'import { useColorScheme } from "react-native";',
3765
+ "",
3766
+ 'import { Colors } from "@/src/constants/Colors";',
3767
+ "",
3768
+ "SplashScreen.preventAutoHideAsync();",
3769
+ "",
3770
+ "export default function RootLayout() {",
3771
+ " const colorScheme = useColorScheme();",
3772
+ " const [loaded] = useFonts({",
3773
+ ' SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),',
3774
+ " });",
3775
+ "",
3776
+ " useEffect(() => {",
3777
+ " if (loaded) {",
3778
+ " SplashScreen.hideAsync();",
3779
+ " }",
3780
+ " }, [loaded]);",
3781
+ "",
3782
+ " if (!loaded) {",
3783
+ " return null;",
3784
+ " }",
3785
+ "",
3786
+ " return (",
3787
+ ' <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>',
3788
+ " <Stack>",
3789
+ ' <Stack.Screen name="login" options={{ headerShown: false }} />',
3790
+ ' <Stack.Screen name="(tabs)" options={{ headerShown: false }} />',
3791
+ ' <Stack.Screen name="+not-found" />',
3792
+ " </Stack>",
3793
+ " </ThemeProvider>",
3794
+ " );",
3795
+ "}",
3796
+ ""
3797
+ )
3798
+ },
3799
+ // ─── app/login.tsx ───────────────────────────────────────────────────
3800
+ {
3801
+ path: "app/login.tsx",
3802
+ content: lines(
3803
+ 'import { SignInForm } from "@/src/components/SignInForm";',
3804
+ 'import { View } from "react-native";',
3805
+ "",
3806
+ "export default function LoginScreen() {",
3807
+ " return (",
3808
+ ' <View className="flex-1 items-center justify-center p-4">',
3809
+ " <SignInForm />",
3810
+ " </View>",
3811
+ " );",
3812
+ "}",
3813
+ ""
3814
+ )
3815
+ },
3816
+ // ─── app/(tabs)/_layout.tsx (overwrite base) ───────────────────────
3817
+ {
3818
+ path: "app/(tabs)/_layout.tsx",
3819
+ content: lines(
3820
+ 'import { Tabs } from "expo-router";',
3821
+ 'import { Platform } from "react-native";',
3822
+ "",
3823
+ 'import { Colors } from "@/src/constants/Colors";',
3824
+ 'import { useColorScheme } from "@/src/hooks/useColorScheme";',
3825
+ "",
3826
+ "export default function TabLayout() {",
3827
+ " const colorScheme = useColorScheme();",
3828
+ "",
3829
+ " return (",
3830
+ " <Tabs",
3831
+ " screenOptions={{",
3832
+ ' tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,',
3833
+ " headerStyle: {",
3834
+ ' backgroundColor: Colors[colorScheme ?? "light"].background,',
3835
+ " },",
3836
+ " headerShadowVisible: false,",
3837
+ " tabBarStyle: Platform.select({",
3838
+ " ios: {",
3839
+ ' position: "absolute",',
3840
+ " },",
3841
+ " default: {},",
3842
+ " }),",
3843
+ " }}",
3844
+ " >",
3845
+ " <Tabs.Screen",
3846
+ ' name="home"',
3847
+ " options={{",
3848
+ ' title: "Home",',
3849
+ " tabBarIcon: () => null,",
3850
+ " }}",
3851
+ " />",
3852
+ " <Tabs.Screen",
3853
+ ' name="list"',
3854
+ " options={{",
3855
+ ' title: "List",',
3856
+ " tabBarIcon: () => null,",
3857
+ " }}",
3858
+ " />",
3859
+ " <Tabs.Screen",
3860
+ ' name="mine"',
3861
+ " options={{",
3862
+ ' title: "Mine",',
3863
+ " tabBarIcon: () => null,",
3864
+ " }}",
3865
+ " />",
3866
+ " </Tabs>",
3867
+ " );",
3868
+ "}",
3869
+ ""
3870
+ )
3871
+ },
3872
+ // ─── app/(tabs)/home.tsx ────────────────────────────────────────────
3873
+ {
3874
+ path: "app/(tabs)/home.tsx",
3875
+ content: lines(
3876
+ 'import { Button } from "@/src/components/ui/button";',
3877
+ "import {",
3878
+ " AlertDialog,",
3879
+ " AlertDialogAction,",
3880
+ " AlertDialogCancel,",
3881
+ " AlertDialogContent,",
3882
+ " AlertDialogDescription,",
3883
+ " AlertDialogFooter,",
3884
+ " AlertDialogHeader,",
3885
+ " AlertDialogTitle,",
3886
+ " AlertDialogTrigger,",
3887
+ '} from "@/src/components/ui/alert-dialog";',
3888
+ 'import { Text } from "@/src/components/ui/text";',
3889
+ 'import { View } from "react-native";',
3890
+ "",
3891
+ "export default function HomeScreen() {",
3892
+ " return (",
3893
+ ' <View className="flex-1 gap-6 p-6">',
3894
+ ' <Text variant="h3">UI Components</Text>',
3895
+ ' <Text variant="muted">React Native Reusables components showcase</Text>',
3896
+ "",
3897
+ " {/* Button variants */}",
3898
+ ' <View className="gap-3">',
3899
+ ' <Text variant="large">Buttons</Text>',
3900
+ ' <View className="flex-row flex-wrap gap-2">',
3901
+ " <Button onPress={() => {}}>",
3902
+ " <Text>Default</Text>",
3903
+ " </Button>",
3904
+ ' <Button variant="secondary" onPress={() => {}}>',
3905
+ " <Text>Secondary</Text>",
3906
+ " </Button>",
3907
+ ' <Button variant="destructive" onPress={() => {}}>',
3908
+ " <Text>Destructive</Text>",
3909
+ " </Button>",
3910
+ ' <Button variant="outline" onPress={() => {}}>',
3911
+ " <Text>Outline</Text>",
3912
+ " </Button>",
3913
+ ' <Button variant="ghost" onPress={() => {}}>',
3914
+ " <Text>Ghost</Text>",
3915
+ " </Button>",
3916
+ ' <Button variant="link" onPress={() => {}}>',
3917
+ " <Text>Link</Text>",
3918
+ " </Button>",
3919
+ " </View>",
3920
+ " </View>",
3921
+ "",
3922
+ " {/* AlertDialog */}",
3923
+ ' <View className="gap-3">',
3924
+ ' <Text variant="large">Alert Dialog</Text>',
3925
+ " <AlertDialog>",
3926
+ " <AlertDialogTrigger asChild>",
3927
+ ' <Button variant="outline">',
3928
+ " <Text>Show Alert</Text>",
3929
+ " </Button>",
3930
+ " </AlertDialogTrigger>",
3931
+ " <AlertDialogContent>",
3932
+ " <AlertDialogHeader>",
3933
+ " <AlertDialogTitle>Are you sure?</AlertDialogTitle>",
3934
+ " <AlertDialogDescription>",
3935
+ " This action cannot be undone. This will permanently delete your account and remove your data from our servers.",
3936
+ " </AlertDialogDescription>",
3937
+ " </AlertDialogHeader>",
3938
+ " <AlertDialogFooter>",
3939
+ " <AlertDialogCancel>",
3940
+ " <Text>Cancel</Text>",
3941
+ " </AlertDialogCancel>",
3942
+ " <AlertDialogAction>",
3943
+ " <Text>Continue</Text>",
3944
+ " </AlertDialogAction>",
3945
+ " </AlertDialogFooter>",
3946
+ " </AlertDialogContent>",
3947
+ " </AlertDialog>",
3948
+ " </View>",
3949
+ " </View>",
3950
+ " );",
3951
+ "}",
3952
+ ""
3953
+ )
3954
+ },
3955
+ // ─── app/(tabs)/list.tsx ────────────────────────────────────────────
3956
+ {
3957
+ path: "app/(tabs)/list.tsx",
3958
+ content: lines(
3959
+ 'import { Text } from "@/src/components/ui/text";',
3960
+ 'import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/src/components/ui/card";',
3961
+ 'import { View, ScrollView } from "react-native";',
3962
+ "",
3963
+ "const ITEMS = Array.from({ length: 20 }, (_, i) => ({",
3964
+ " id: String(i + 1),",
3965
+ " title: `Item ${i + 1}`,",
3966
+ " description: `Description for item ${i + 1}`,",
3967
+ "}));",
3968
+ "",
3969
+ "export default function ListScreen() {",
3970
+ " return (",
3971
+ ' <ScrollView className="flex-1 p-4">',
3972
+ ' <Text variant="h3" className="mb-4">List</Text>',
3973
+ ' <View className="gap-3">',
3974
+ " {ITEMS.map((item) => (",
3975
+ " <Card key={item.id}>",
3976
+ " <CardHeader>",
3977
+ " <CardTitle>{item.title}</CardTitle>",
3978
+ " <CardDescription>{item.description}</CardDescription>",
3979
+ " </CardHeader>",
3980
+ " <CardContent />",
3981
+ " </Card>",
3982
+ " ))}",
3983
+ " </View>",
3984
+ " </ScrollView>",
3985
+ " );",
3986
+ "}",
3987
+ ""
3988
+ )
3989
+ },
3990
+ // ─── app/(tabs)/mine.tsx ────────────────────────────────────────────
3991
+ {
3992
+ path: "app/(tabs)/mine.tsx",
3993
+ content: lines(
3994
+ 'import { Button } from "@/src/components/ui/button";',
3995
+ 'import { Card, CardContent, CardHeader, CardTitle } from "@/src/components/ui/card";',
3996
+ 'import { Text } from "@/src/components/ui/text";',
3997
+ 'import { View } from "react-native";',
3998
+ 'import { router } from "expo-router";',
3999
+ "",
4000
+ "export default function MineScreen() {",
4001
+ " return (",
4002
+ ' <View className="flex-1 p-6">',
4003
+ ' <Text variant="h3" className="mb-6">Mine</Text>',
4004
+ "",
4005
+ ' <Card className="mb-6">',
4006
+ " <CardHeader>",
4007
+ " <CardTitle>Profile</CardTitle>",
4008
+ " </CardHeader>",
4009
+ " <CardContent>",
4010
+ ' <View className="gap-2">',
4011
+ ' <Text variant="muted">User Name</Text>',
4012
+ " <Text>user@example.com</Text>",
4013
+ " </View>",
4014
+ " </CardContent>",
4015
+ " </Card>",
4016
+ "",
4017
+ " <Button",
4018
+ ' variant="destructive"',
4019
+ ' className="w-full"',
4020
+ " onPress={() => {",
4021
+ ' router.replace("/login");',
4022
+ " }}",
4023
+ " >",
4024
+ " <Text>Sign Out</Text>",
4025
+ " </Button>",
4026
+ " </View>",
4027
+ " );",
4028
+ "}",
4029
+ ""
4030
+ )
4031
+ },
4032
+ // ─── src/components/SignInForm.tsx ───────────────────────────────────
4033
+ {
4034
+ path: "src/components/SignInForm.tsx",
4035
+ content: lines(
4036
+ 'import { Button } from "@/src/components/ui/button";',
4037
+ "import {",
4038
+ " Card,",
4039
+ " CardContent,",
4040
+ " CardDescription,",
4041
+ " CardHeader,",
4042
+ " CardTitle,",
4043
+ '} from "@/src/components/ui/card";',
4044
+ 'import { Input } from "@/src/components/ui/input";',
4045
+ 'import { Label } from "@/src/components/ui/label";',
4046
+ 'import { Separator } from "@/src/components/ui/separator";',
4047
+ 'import { Text } from "@/src/components/ui/text";',
4048
+ 'import * as React from "react";',
4049
+ 'import { Pressable, type TextInput, View } from "react-native";',
4050
+ 'import { router } from "expo-router";',
4051
+ "",
4052
+ "export function SignInForm() {",
4053
+ " const passwordInputRef = React.useRef<TextInput>(null);",
4054
+ "",
4055
+ " function onEmailSubmitEditing() {",
4056
+ " passwordInputRef.current?.focus();",
4057
+ " }",
4058
+ "",
4059
+ " function onSubmit() {",
4060
+ " // TODO: Submit form and navigate to protected screen if successful",
4061
+ ' router.replace("/(tabs)/home");',
4062
+ " }",
4063
+ "",
4064
+ " return (",
4065
+ ' <View className="gap-6 w-full max-w-sm">',
4066
+ ' <Card className="shadow-none sm:shadow-sm sm:shadow-black/5">',
4067
+ " <CardHeader>",
4068
+ ' <CardTitle className="text-center text-xl sm:text-left">Sign in to your app</CardTitle>',
4069
+ ' <CardDescription className="text-center sm:text-left">',
4070
+ " Welcome back! Please sign in to continue",
4071
+ " </CardDescription>",
4072
+ " </CardHeader>",
4073
+ ' <CardContent className="gap-6">',
4074
+ ' <View className="gap-6">',
4075
+ ' <View className="gap-1.5">',
4076
+ ' <Label nativeID="email">Email</Label>',
4077
+ " <Input",
4078
+ ' placeholder="m@example.com"',
4079
+ ' keyboardType="email-address"',
4080
+ ' autoComplete="email"',
4081
+ ' autoCapitalize="none"',
4082
+ " onSubmitEditing={onEmailSubmitEditing}",
4083
+ ' returnKeyType="next"',
4084
+ ' submitBehavior="submit"',
4085
+ " />",
4086
+ " </View>",
4087
+ ' <View className="gap-1.5">',
4088
+ ' <View className="flex-row items-center">',
4089
+ ' <Label nativeID="password">Password</Label>',
4090
+ " <Button",
4091
+ ' variant="link"',
4092
+ ' size="sm"',
4093
+ ' className="ml-auto h-4 px-1 py-0 sm:h-4"',
4094
+ " onPress={() => {",
4095
+ " // TODO: Navigate to forgot password screen",
4096
+ " }}",
4097
+ " >",
4098
+ ' <Text className="font-normal leading-4">Forgot your password?</Text>',
4099
+ " </Button>",
4100
+ " </View>",
4101
+ " <Input",
4102
+ " ref={passwordInputRef}",
4103
+ " secureTextEntry",
4104
+ ' returnKeyType="send"',
4105
+ " onSubmitEditing={onSubmit}",
4106
+ " />",
4107
+ " </View>",
4108
+ ' <Button className="w-full" onPress={onSubmit}>',
4109
+ " <Text>Continue</Text>",
4110
+ " </Button>",
4111
+ " </View>",
4112
+ ' <Text className="text-center text-sm">',
4113
+ ' Don&apos;t have an account?{" "}',
4114
+ " <Pressable",
4115
+ " onPress={() => {",
4116
+ " // TODO: Navigate to sign up screen",
4117
+ " }}",
4118
+ " >",
4119
+ ' <Text className="text-sm underline underline-offset-4">Sign up</Text>',
4120
+ " </Pressable>",
4121
+ " </Text>",
4122
+ ' <View className="flex-row items-center">',
4123
+ ' <Separator className="flex-1" />',
4124
+ ' <Text className="text-muted-foreground px-4 text-sm">or</Text>',
4125
+ ' <Separator className="flex-1" />',
4126
+ " </View>",
4127
+ " </CardContent>",
4128
+ " </Card>",
4129
+ " </View>",
4130
+ " );",
4131
+ "}",
4132
+ ""
4133
+ )
4134
+ }
4135
+ ];
4136
+ }
4137
+
3329
4138
  // src/utils/file.ts
3330
4139
  var import_fs_extra = __toESM(require("fs-extra"));
3331
4140
  var import_path = __toESM(require("path"));
@@ -3388,7 +4197,8 @@ function generateBasePackageJson(projectName) {
3388
4197
  "react-native-safe-area-context": "~5.6.0",
3389
4198
  "react-native-screens": "~4.16.0",
3390
4199
  nativewind: "^4.1.0",
3391
- tailwindcss: "^3.4.0"
4200
+ tailwindcss: "^3.4.0",
4201
+ "react-native-svg": "^15.8.0"
3392
4202
  },
3393
4203
  devDependencies: {
3394
4204
  "@types/react": "~19.1.0",
@@ -3399,7 +4209,7 @@ function generateBasePackageJson(projectName) {
3399
4209
 
3400
4210
  // src/index.ts
3401
4211
  var import_execa = require("execa");
3402
- var CLI_VERSION = "1.2.0";
4212
+ var CLI_VERSION = "1.3.0";
3403
4213
  var CONFIG_FILE = ".expo-bbase.json";
3404
4214
  async function run() {
3405
4215
  const program = new import_commander.Command();
@@ -3440,10 +4250,34 @@ async function createProject(projectName) {
3440
4250
  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
4251
  );
3442
4252
  console.log();
4253
+ const { uiTemplate } = await (0, import_prompts.default)({
4254
+ type: "select",
4255
+ name: "uiTemplate",
4256
+ message: "Choose a UI template",
4257
+ choices: [
4258
+ {
4259
+ title: `${import_chalk.default.bold("Login + Tabs")} \u2014 Login page, Home/List/Mine tabs with rnr components`,
4260
+ value: "login-tabs",
4261
+ description: "Pre-built login form, 3-tab layout with Button & AlertDialog demos"
4262
+ },
4263
+ {
4264
+ title: `${import_chalk.default.bold("Default")} \u2014 Blank tabs (Home + Explore)`,
4265
+ value: "default",
4266
+ description: "Minimal starter with basic tab navigation"
4267
+ }
4268
+ ],
4269
+ initial: 0
4270
+ });
4271
+ if (uiTemplate === void 0) {
4272
+ console.log(import_chalk.default.yellow("\nCancelled."));
4273
+ process.exit(0);
4274
+ }
4275
+ const isLoginTabs = uiTemplate === "login-tabs";
3443
4276
  const choices = modules.map((m) => ({
3444
4277
  title: `${import_chalk.default.bold(m.name)} \u2014 ${import_chalk.default.gray(m.description)}`,
3445
4278
  value: m.id,
3446
- selected: m.defaultChecked
4279
+ // Auto-select ui-reusables when login-tabs template is chosen
4280
+ selected: isLoginTabs && m.id === "ui-reusables" ? true : m.defaultChecked
3447
4281
  }));
3448
4282
  const { selectedModules } = await (0, import_prompts.default)({
3449
4283
  type: "multiselect",
@@ -3457,11 +4291,20 @@ async function createProject(projectName) {
3457
4291
  console.log(import_chalk.default.yellow("\nCancelled."));
3458
4292
  process.exit(0);
3459
4293
  }
3460
- const selectedModuleDefs = getModulesByIds(selectedModules);
4294
+ let finalModuleIds = selectedModules;
4295
+ if (isLoginTabs && !finalModuleIds.includes("ui-reusables")) {
4296
+ finalModuleIds = ["ui-reusables", ...finalModuleIds];
4297
+ }
4298
+ const selectedModuleDefs = getModulesByIds(finalModuleIds);
3461
4299
  const targetDir = import_path2.default.resolve(process.cwd(), projectName);
3462
4300
  console.log();
3463
4301
  console.log(import_chalk.default.white(` \u{1F4E6} Project: ${import_chalk.default.bold(projectName)}`));
3464
4302
  console.log(import_chalk.default.white(` \u{1F4C2} Path: ${import_chalk.default.gray(targetDir)}`));
4303
+ console.log(
4304
+ import_chalk.default.white(
4305
+ ` \u{1F3A8} Template: ${import_chalk.default.green(isLoginTabs ? "Login + Tabs" : "Default")}`
4306
+ )
4307
+ );
3465
4308
  console.log(
3466
4309
  import_chalk.default.white(
3467
4310
  ` \u{1F9E9} Modules: ${import_chalk.default.green(selectedModuleDefs.map((m) => m.name).join(", ") || "none")}`
@@ -3484,6 +4327,15 @@ async function createProject(projectName) {
3484
4327
  await writeFile(filePath, content);
3485
4328
  }
3486
4329
  }
4330
+ if (isLoginTabs) {
4331
+ spinner.text = "Writing UI template files...";
4332
+ const loginTabsTemplates = generateLoginTabsTemplates(projectName);
4333
+ for (const file of loginTabsTemplates) {
4334
+ const filePath = import_path2.default.join(targetDir, file.path);
4335
+ const content = replaceTemplateVars(file.content, { projectName });
4336
+ await writeFile(filePath, content);
4337
+ }
4338
+ }
3487
4339
  spinner.text = "Generating package.json...";
3488
4340
  const pkgJson = generateBasePackageJson(projectName);
3489
4341
  const allDeps = {};
@@ -3507,8 +4359,9 @@ async function createProject(projectName) {
3507
4359
  await updateLayoutFile(targetDir, selectedModuleDefs);
3508
4360
  await writeProjectConfig(targetDir, {
3509
4361
  projectName,
3510
- selectedModules,
3511
- cliVersion: CLI_VERSION
4362
+ selectedModules: finalModuleIds,
4363
+ cliVersion: CLI_VERSION,
4364
+ uiTemplate: isLoginTabs ? "login-tabs" : "default"
3512
4365
  });
3513
4366
  spinner.text = "Installing dependencies (yarn install)...";
3514
4367
  try {
@@ -3527,6 +4380,9 @@ async function createProject(projectName) {
3527
4380
  console.log(import_chalk.default.bold(" \u{1F389} Next steps:"));
3528
4381
  console.log(import_chalk.default.white(` cd ${projectName}`));
3529
4382
  console.log(import_chalk.default.white(" npx expo start"));
4383
+ if (isLoginTabs) {
4384
+ console.log(import_chalk.default.gray(" \u2192 App starts at login page, sign in to see tabs"));
4385
+ }
3530
4386
  console.log();
3531
4387
  if (selectedModuleDefs.length > 0) {
3532
4388
  console.log(import_chalk.default.bold(" \u{1F4CB} Selected modules:"));