expo-bbase 1.1.1 → 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
@@ -30,8 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ addModule: () => addModule,
33
34
  createProject: () => createProject,
34
- run: () => run
35
+ run: () => run,
36
+ upgradeProject: () => upgradeProject
35
37
  });
36
38
  module.exports = __toCommonJS(src_exports);
37
39
  var import_chalk = __toESM(require("chalk"));
@@ -47,6 +49,16 @@ function registerCreateCommand(program) {
47
49
  await createProject(projectName);
48
50
  });
49
51
  }
52
+ function registerUpgradeCommand(program) {
53
+ program.command("upgrade").description("Upgrade an existing expo-bbase project to the latest module versions").option("--dir <path>", "Project directory (default: current directory)", process.cwd()).action(async (options) => {
54
+ await upgradeProject(options.dir);
55
+ });
56
+ }
57
+ function registerAddCommand(program) {
58
+ program.command("add [modules...]").description("Add one or more modules to an existing expo-bbase project").option("--dir <path>", "Project directory (default: current directory)", process.cwd()).action(async (moduleIds, options) => {
59
+ await addModule(moduleIds, options.dir);
60
+ });
61
+ }
50
62
 
51
63
  // src/utils/lines.ts
52
64
  function lines(...args) {
@@ -2508,267 +2520,594 @@ var flashlist_default = flashlistModule;
2508
2520
  var uiReusablesModule = {
2509
2521
  id: "ui-reusables",
2510
2522
  name: "reactnative.reusables UI",
2511
- description: "\u9884\u7F6E UI \u7EC4\u4EF6",
2523
+ description: "\u9884\u7F6E UI \u7EC4\u4EF6 (Button, AlertDialog, Card, Input, Label, Text, Separator)",
2512
2524
  defaultChecked: false,
2513
2525
  dependencies: {
2514
- "reactnative.reusables": "^0.1.0",
2515
- "react-native-svg": "^15.8.0",
2526
+ "class-variance-authority": "^0.7.1",
2527
+ "clsx": "^2.1.1",
2528
+ "tailwind-merge": "^2.6.0",
2516
2529
  "@rn-primitives/slot": "^1.1.0",
2517
- "@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"
2518
2535
  },
2519
2536
  devDependencies: {},
2520
2537
  files: [
2538
+ // ─── lib/utils.ts ────────────────────────────────────────────────────
2521
2539
  {
2522
- path: "src/components/ui/button.tsx",
2540
+ path: "src/lib/utils.ts",
2523
2541
  content: lines(
2524
- 'import React from "react";',
2525
- 'import { Text, Pressable, type PressableProps, type StyleProp, type ViewStyle, type TextStyle } from "react-native";',
2526
- 'import { Slot } from "@rn-primitives/slot";',
2527
- "",
2528
- 'type ButtonVariant = "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";',
2529
- 'type ButtonSize = "default" | "sm" | "lg" | "icon";',
2542
+ 'import { type ClassValue, clsx } from "clsx";',
2543
+ 'import { twMerge } from "tailwind-merge";',
2530
2544
  "",
2531
- "interface ButtonProps extends PressableProps {",
2532
- " variant?: ButtonVariant;",
2533
- " size?: ButtonSize;",
2534
- " asChild?: boolean;",
2535
- " style?: StyleProp<ViewStyle>;",
2536
- " textStyle?: StyleProp<TextStyle>;",
2537
- " children: React.ReactNode;",
2545
+ "export function cn(...inputs: ClassValue[]) {",
2546
+ " return twMerge(clsx(inputs));",
2538
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
+ ");",
2539
2604
  "",
2540
- "const variantStyles: Record<ButtonVariant, ViewStyle> = {",
2541
- ' default: { backgroundColor: "#0f172a" },',
2542
- ' destructive: { backgroundColor: "#ef4444" },',
2543
- ' outline: { backgroundColor: "transparent", borderWidth: 1, borderColor: "#d4d4d8" },',
2544
- ' secondary: { backgroundColor: "#f4f4f5" },',
2545
- ' ghost: { backgroundColor: "transparent" },',
2546
- ' link: { backgroundColor: "transparent" },',
2547
- "};",
2605
+ "type TextVariantProps = VariantProps<typeof textVariants>;",
2548
2606
  "",
2549
- "const variantTextStyles: Record<ButtonVariant, TextStyle> = {",
2550
- ' default: { color: "#fafafa" },',
2551
- ' destructive: { color: "#fafafa" },',
2552
- ' outline: { color: "#18181b" },',
2553
- ' secondary: { color: "#18181b" },',
2554
- ' ghost: { color: "#18181b" },',
2555
- ' link: { color: "#2563eb", textDecorationLine: "underline" },',
2556
- "};",
2607
+ 'type TextVariant = NonNullable<TextVariantProps["variant"]>;',
2557
2608
  "",
2558
- "const sizeStyles: Record<ButtonSize, ViewStyle> = {",
2559
- " default: { height: 44, paddingHorizontal: 16 },",
2560
- " sm: { height: 36, paddingHorizontal: 12 },",
2561
- " lg: { height: 52, paddingHorizontal: 24 },",
2562
- " 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 }),',
2563
2616
  "};",
2564
2617
  "",
2565
- "const sizeTextStyles: Record<ButtonSize, TextStyle> = {",
2566
- " default: { fontSize: 16 },",
2567
- " sm: { fontSize: 14 },",
2568
- " lg: { fontSize: 18 },",
2569
- " icon: { fontSize: 16 },",
2618
+ "const ARIA_LEVEL: Partial<Record<TextVariant, string>> = {",
2619
+ ' h1: "1",',
2620
+ ' h2: "2",',
2621
+ ' h3: "3",',
2622
+ ' h4: "4",',
2570
2623
  "};",
2571
2624
  "",
2572
- "export function Button({",
2573
- ' variant = "default",',
2574
- ' size = "default",',
2625
+ "const TextClassContext = React.createContext<string | undefined>(undefined);",
2626
+ "",
2627
+ "function Text({",
2628
+ " className,",
2575
2629
  " asChild = false,",
2576
- " style,",
2577
- " textStyle,",
2578
- " children,",
2579
- " ...rest",
2580
- "}: ButtonProps) {",
2581
- " const containerStyle: StyleProp<ViewStyle> = [",
2582
- " {",
2583
- " borderRadius: 8,",
2584
- ' flexDirection: "row",',
2585
- ' alignItems: "center",',
2586
- ' justifyContent: "center",',
2587
- " },",
2588
- " variantStyles[variant],",
2589
- " sizeStyles[size],",
2590
- " style,",
2591
- " ];",
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
+ "}",
2592
2646
  "",
2593
- " const textStyling: StyleProp<TextStyle> = [",
2594
- " {",
2595
- ' fontWeight: "600",',
2596
- ' 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",',
2597
2715
  " },",
2598
- " variantTextStyles[variant],",
2599
- " sizeTextStyles[size],",
2600
- " textStyle,",
2601
- " ];",
2716
+ " }",
2717
+ ");",
2602
2718
  "",
2603
- " if (asChild && React.isValidElement(children)) {",
2604
- " return (",
2605
- " <Pressable style={containerStyle} {...rest}>",
2606
- " <Slot>{children}</Slot>",
2607
- " </Pressable>",
2608
- " );",
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
+ " },",
2609
2751
  " }",
2752
+ ");",
2753
+ "",
2754
+ "type ButtonProps = React.ComponentProps<typeof Pressable> &",
2755
+ " React.RefAttributes<typeof Pressable> &",
2756
+ " VariantProps<typeof buttonVariants>;",
2610
2757
  "",
2758
+ "function Button({ className, variant, size, ...props }: ButtonProps) {",
2611
2759
  " return (",
2612
- " <Pressable style={containerStyle} {...rest}>",
2613
- ' {typeof children === "string" ? (',
2614
- " <Text style={textStyling}>{children}</Text>",
2615
- " ) : (",
2616
- " children",
2617
- " )}",
2618
- " </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>",
2619
2767
  " );",
2620
2768
  "}",
2621
2769
  "",
2622
- "export default Button;"
2770
+ "export { Button, buttonTextVariants, buttonVariants };",
2771
+ "export type { ButtonProps };",
2772
+ ""
2623
2773
  )
2624
2774
  },
2775
+ // ─── components/ui/input.tsx ──────────────────────────────────────────
2625
2776
  {
2626
2777
  path: "src/components/ui/input.tsx",
2627
2778
  content: lines(
2628
- 'import React, { forwardRef } from "react";',
2629
- "import {",
2630
- " TextInput,",
2631
- " type TextInputProps,",
2632
- " type StyleProp,",
2633
- " type ViewStyle,",
2634
- " type TextStyle,",
2635
- " View,",
2636
- " Text,",
2637
- '} from "react-native";',
2638
- "",
2639
- "interface InputProps extends TextInputProps {",
2640
- " label?: string;",
2641
- " error?: string;",
2642
- " containerStyle?: StyleProp<ViewStyle>;",
2643
- " inputStyle?: StyleProp<TextStyle>;",
2644
- "}",
2645
- "",
2646
- "export const Input = forwardRef<TextInput, InputProps>(",
2647
- " ({ label, error, containerStyle, inputStyle, ...rest }, ref) => {",
2648
- " return (",
2649
- " <View style={containerStyle}>",
2650
- " {label && <Text style={styles.label}>{label}</Text>}",
2651
- " <TextInput",
2652
- " ref={ref}",
2653
- " style={[",
2654
- " styles.input,",
2655
- " error && styles.inputError,",
2656
- " inputStyle,",
2657
- " ]}",
2658
- ' placeholderTextColor="#a1a1aa"',
2659
- " {...rest}",
2660
- " />",
2661
- " {error && <Text style={styles.errorText}>{error}</Text>}",
2662
- " </View>",
2663
- " );",
2664
- " }",
2665
- ");",
2779
+ 'import { cn } from "@/src/lib/utils";',
2780
+ 'import { Platform, TextInput } from "react-native";',
2666
2781
  "",
2667
- 'Input.displayName = "Input";',
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
+ "}",
2668
2809
  "",
2669
- "const styles = {",
2670
- " label: {",
2671
- " fontSize: 14,",
2672
- ' fontWeight: "500",',
2673
- ' color: "#18181b",',
2674
- " marginBottom: 6,",
2675
- " } as TextStyle,",
2676
- " input: {",
2677
- " height: 44,",
2678
- " borderWidth: 1,",
2679
- ' borderColor: "#d4d4d8",',
2680
- " borderRadius: 8,",
2681
- " paddingHorizontal: 12,",
2682
- " fontSize: 16,",
2683
- ' color: "#18181b",',
2684
- ' backgroundColor: "#ffffff",',
2685
- " } as TextStyle,",
2686
- " inputError: {",
2687
- ' borderColor: "#ef4444",',
2688
- " } as TextStyle,",
2689
- " errorText: {",
2690
- " fontSize: 12,",
2691
- ' color: "#ef4444",',
2692
- " marginTop: 4,",
2693
- " } as TextStyle,",
2694
- "};",
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";',
2821
+ "",
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
+ "}",
2695
2857
  "",
2696
- "export default Input;"
2858
+ "export { Label };",
2859
+ ""
2697
2860
  )
2698
2861
  },
2862
+ // ─── components/ui/card.tsx ──────────────────────────────────────────
2699
2863
  {
2700
2864
  path: "src/components/ui/card.tsx",
2701
2865
  content: lines(
2702
- 'import React from "react";',
2703
- '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
+ "}",
2704
2886
  "",
2705
- "interface CardProps {",
2706
- " title?: string;",
2707
- " description?: string;",
2708
- " children: React.ReactNode;",
2709
- " style?: StyleProp<ViewStyle>;",
2710
- " titleStyle?: StyleProp<TextStyle>;",
2711
- " descriptionStyle?: StyleProp<TextStyle>;",
2712
- ' 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
+ " );",
2713
2894
  "}",
2714
2895
  "",
2715
- 'const paddingMap: Record<NonNullable<CardProps["padding"]>, number> = {',
2716
- " none: 0,",
2717
- " sm: 8,",
2718
- " md: 16,",
2719
- " lg: 24,",
2720
- "};",
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
+ "}",
2721
2911
  "",
2722
- "export function Card({",
2723
- " title,",
2724
- " description,",
2725
- " children,",
2726
- " style,",
2727
- " titleStyle,",
2728
- " descriptionStyle,",
2729
- ' padding = "md",',
2730
- "}: CardProps) {",
2912
+ "function CardDescription({",
2913
+ " className,",
2914
+ " ...props",
2915
+ "}: React.ComponentProps<typeof Text> & React.RefAttributes<typeof Text>) {",
2731
2916
  " return (",
2732
- " <View",
2733
- " style={[",
2734
- " {",
2735
- ' backgroundColor: "#ffffff",',
2736
- " borderRadius: 12,",
2737
- " borderWidth: 1,",
2738
- ' borderColor: "#e4e4e7",',
2739
- " padding: paddingMap[padding],",
2740
- " },",
2741
- " style,",
2742
- " ]}",
2743
- " >",
2744
- " {title && (",
2745
- " <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",
2746
2964
  " )}",
2747
- " {description && (",
2748
- " <Text style={[styles.description, descriptionStyle]}>",
2749
- " {description}",
2750
- " </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",
2751
3005
  " )}",
2752
- " {children}",
2753
- " </View>",
3006
+ " {...props}",
3007
+ " />",
2754
3008
  " );",
2755
3009
  "}",
2756
3010
  "",
2757
- "const styles = {",
2758
- " title: {",
2759
- " fontSize: 18,",
2760
- ' fontWeight: "600",',
2761
- ' color: "#18181b",',
2762
- " marginBottom: 4,",
2763
- " } as TextStyle,",
2764
- " description: {",
2765
- " fontSize: 14,",
2766
- ' color: "#71717a",',
2767
- " marginBottom: 12,",
2768
- " } as TextStyle,",
2769
- "};",
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
+ "}",
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
+ "}",
2770
3084
  "",
2771
- "export default Card;"
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
+ ""
2772
3111
  )
2773
3112
  }
2774
3113
  ]
@@ -2814,7 +3153,8 @@ function generateBaseTemplates(projectName) {
2814
3153
  // ─── app/_layout.tsx ────────────────────────────────────────────────
2815
3154
  {
2816
3155
  path: "app/_layout.tsx",
2817
- content: `import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
3156
+ content: `import "../global.css";
3157
+ import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
2818
3158
  import { useFonts } from "expo-font";
2819
3159
  import { Stack } from "expo-router";
2820
3160
  import * as SplashScreen from "expo-splash-screen";
@@ -3239,8 +3579,45 @@ module.exports = {
3239
3579
  "./app/**/*.{js,jsx,ts,tsx}",
3240
3580
  "./src/**/*.{js,jsx,ts,tsx}",
3241
3581
  ],
3582
+ presets: [require("nativewind/preset")],
3242
3583
  theme: {
3243
- 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
+ },
3244
3621
  },
3245
3622
  plugins: [],
3246
3623
  };
@@ -3250,10 +3627,11 @@ module.exports = {
3250
3627
  {
3251
3628
  path: "metro.config.js",
3252
3629
  content: `const { getDefaultConfig } = require("expo/metro-config");
3630
+ const { withNativeWind } = require("nativewind/metro");
3253
3631
 
3254
3632
  const config = getDefaultConfig(__dirname);
3255
3633
 
3256
- module.exports = config;
3634
+ module.exports = withNativeWind(config, { input: "./global.css" });
3257
3635
  `
3258
3636
  },
3259
3637
  // ─── babel.config.js ─────────────────────────────────────────────────
@@ -3262,10 +3640,67 @@ module.exports = config;
3262
3640
  content: `module.exports = function (api) {
3263
3641
  api.cache(true);
3264
3642
  return {
3265
- presets: ["babel-preset-expo"],
3266
- plugins: ["nativewind/babel"],
3643
+ presets: [
3644
+ ["babel-preset-expo", { jsxImportSource: "nativewind" }],
3645
+ "nativewind/babel",
3646
+ ],
3647
+ plugins: ["react-native-reanimated/plugin"],
3267
3648
  };
3268
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
+ }
3269
3704
  `
3270
3705
  },
3271
3706
  // ─── .gitignore ─────────────────────────────────────────────────────
@@ -3314,6 +3749,392 @@ expo-env.d.ts
3314
3749
  ];
3315
3750
  }
3316
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
+
3317
4138
  // src/utils/file.ts
3318
4139
  var import_fs_extra = __toESM(require("fs-extra"));
3319
4140
  var import_path = __toESM(require("path"));
@@ -3322,6 +4143,9 @@ async function writeFile(filePath, content) {
3322
4143
  await import_fs_extra.default.ensureDir(dir);
3323
4144
  await import_fs_extra.default.writeFile(filePath, content, "utf-8");
3324
4145
  }
4146
+ async function readJson(filePath) {
4147
+ return import_fs_extra.default.readJson(filePath);
4148
+ }
3325
4149
  async function writeJson(filePath, data, spaces = 2) {
3326
4150
  const dir = import_path.default.dirname(filePath);
3327
4151
  await import_fs_extra.default.ensureDir(dir);
@@ -3337,6 +4161,18 @@ function replaceTemplateVars(content, vars) {
3337
4161
  }
3338
4162
 
3339
4163
  // src/utils/package.ts
4164
+ async function mergeDependencies(pkgPath, deps, devDeps) {
4165
+ const pkg = await readJson(pkgPath);
4166
+ pkg.dependencies = {
4167
+ ...pkg.dependencies || {},
4168
+ ...deps
4169
+ };
4170
+ pkg.devDependencies = {
4171
+ ...pkg.devDependencies || {},
4172
+ ...devDeps
4173
+ };
4174
+ await writeJson(pkgPath, pkg);
4175
+ }
3340
4176
  function generateBasePackageJson(projectName) {
3341
4177
  return {
3342
4178
  name: projectName,
@@ -3361,7 +4197,8 @@ function generateBasePackageJson(projectName) {
3361
4197
  "react-native-safe-area-context": "~5.6.0",
3362
4198
  "react-native-screens": "~4.16.0",
3363
4199
  nativewind: "^4.1.0",
3364
- tailwindcss: "^3.4.0"
4200
+ tailwindcss: "^3.4.0",
4201
+ "react-native-svg": "^15.8.0"
3365
4202
  },
3366
4203
  devDependencies: {
3367
4204
  "@types/react": "~19.1.0",
@@ -3372,9 +4209,11 @@ function generateBasePackageJson(projectName) {
3372
4209
 
3373
4210
  // src/index.ts
3374
4211
  var import_execa = require("execa");
4212
+ var CLI_VERSION = "1.3.0";
4213
+ var CONFIG_FILE = ".expo-bbase.json";
3375
4214
  async function run() {
3376
4215
  const program = new import_commander.Command();
3377
- program.name("expo-bbase").description("Expo SDK 54+ \u811A\u624B\u67B6 CLI \u5DE5\u5177").version("1.0.0");
4216
+ program.name("expo-bbase").description("Expo SDK 54+ scaffolding CLI tool").version(CLI_VERSION);
3378
4217
  program.argument("[project-name]", "Name of the project to create").action(async (projectName) => {
3379
4218
  if (!projectName) {
3380
4219
  console.error(import_chalk.default.red("Error: Please provide a project name."));
@@ -3384,71 +4223,120 @@ async function run() {
3384
4223
  await createProject(projectName);
3385
4224
  });
3386
4225
  registerCreateCommand(program);
4226
+ registerUpgradeCommand(program);
4227
+ registerAddCommand(program);
3387
4228
  await program.parseAsync(process.argv);
3388
4229
  }
4230
+ async function readProjectConfig(targetDir) {
4231
+ const configPath = import_path2.default.join(targetDir, CONFIG_FILE);
4232
+ if (!await import_fs_extra2.default.pathExists(configPath)) {
4233
+ return null;
4234
+ }
4235
+ return import_fs_extra2.default.readJson(configPath);
4236
+ }
4237
+ async function writeProjectConfig(targetDir, config) {
4238
+ const configPath = import_path2.default.join(targetDir, CONFIG_FILE);
4239
+ await writeJson(configPath, config);
4240
+ }
3389
4241
  async function createProject(projectName) {
3390
4242
  console.log();
3391
4243
  console.log(
3392
4244
  import_chalk.default.bold.cyan(" \u2554\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\u2557")
3393
4245
  );
3394
4246
  console.log(
3395
- import_chalk.default.bold.cyan(" \u2551 expo-bbase \u2014 Expo \u811A\u624B\u67B6\u5DE5\u5177 \u2551")
4247
+ import_chalk.default.bold.cyan(" \u2551 expo-bbase \u2014 Expo Scaffolding \u2551")
3396
4248
  );
3397
4249
  console.log(
3398
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")
3399
4251
  );
3400
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";
3401
4276
  const choices = modules.map((m) => ({
3402
4277
  title: `${import_chalk.default.bold(m.name)} \u2014 ${import_chalk.default.gray(m.description)}`,
3403
4278
  value: m.id,
3404
- selected: m.defaultChecked
4279
+ // Auto-select ui-reusables when login-tabs template is chosen
4280
+ selected: isLoginTabs && m.id === "ui-reusables" ? true : m.defaultChecked
3405
4281
  }));
3406
4282
  const { selectedModules } = await (0, import_prompts.default)({
3407
4283
  type: "multiselect",
3408
4284
  name: "selectedModules",
3409
- message: "\u9009\u62E9\u9700\u8981\u7684\u529F\u80FD\u6A21\u5757\uFF08\u7A7A\u683C\u5207\u6362\uFF0C\u56DE\u8F66\u786E\u8BA4\uFF09",
4285
+ message: "Select modules (Space to toggle, Enter to confirm)",
3410
4286
  choices,
3411
- hint: "- \u7A7A\u683C\u5207\u6362\u9009\u62E9 \xB7 a \u5168\u9009/\u53D6\u6D88 \xB7 \u56DE\u8F66\u786E\u8BA4",
4287
+ hint: "- Space toggle \xB7 a select all/none \xB7 Enter confirm",
3412
4288
  instructions: false
3413
4289
  });
3414
4290
  if (selectedModules === void 0) {
3415
- console.log(import_chalk.default.yellow("\n\u5DF2\u53D6\u6D88\u521B\u5EFA\u9879\u76EE\u3002"));
4291
+ console.log(import_chalk.default.yellow("\nCancelled."));
3416
4292
  process.exit(0);
3417
4293
  }
3418
- 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);
3419
4299
  const targetDir = import_path2.default.resolve(process.cwd(), projectName);
3420
4300
  console.log();
3421
- console.log(import_chalk.default.white(` \u{1F4E6} \u9879\u76EE\u540D\u79F0: ${import_chalk.default.bold(projectName)}`));
4301
+ console.log(import_chalk.default.white(` \u{1F4E6} Project: ${import_chalk.default.bold(projectName)}`));
4302
+ console.log(import_chalk.default.white(` \u{1F4C2} Path: ${import_chalk.default.gray(targetDir)}`));
3422
4303
  console.log(
3423
- import_chalk.default.white(` \u{1F4C2} \u76EE\u6807\u8DEF\u5F84: ${import_chalk.default.gray(targetDir)}`)
4304
+ import_chalk.default.white(
4305
+ ` \u{1F3A8} Template: ${import_chalk.default.green(isLoginTabs ? "Login + Tabs" : "Default")}`
4306
+ )
3424
4307
  );
3425
4308
  console.log(
3426
4309
  import_chalk.default.white(
3427
- ` \u{1F9E9} \u9009\u62E9\u6A21\u5757: ${import_chalk.default.green(selectedModuleDefs.map((m) => m.name).join(", ") || "\u65E0")}`
4310
+ ` \u{1F9E9} Modules: ${import_chalk.default.green(selectedModuleDefs.map((m) => m.name).join(", ") || "none")}`
3428
4311
  )
3429
4312
  );
3430
4313
  console.log();
3431
- const spinner = (0, import_ora.default)("\u6B63\u5728\u521B\u5EFA\u9879\u76EE...").start();
4314
+ const spinner = (0, import_ora.default)("Creating project...").start();
3432
4315
  try {
3433
4316
  const baseTemplates = generateBaseTemplates(projectName);
3434
4317
  for (const file of baseTemplates) {
3435
4318
  const filePath = import_path2.default.join(targetDir, file.path);
3436
- const content = replaceTemplateVars(file.content, {
3437
- projectName
3438
- });
4319
+ const content = replaceTemplateVars(file.content, { projectName });
3439
4320
  await writeFile(filePath, content);
3440
4321
  }
3441
- spinner.text = "\u6B63\u5728\u5199\u5165\u6A21\u5757\u6587\u4EF6...";
4322
+ spinner.text = "Writing module files...";
3442
4323
  for (const mod of selectedModuleDefs) {
3443
4324
  for (const file of mod.files) {
3444
4325
  const filePath = import_path2.default.join(targetDir, file.path);
3445
- const content = replaceTemplateVars(file.content, {
3446
- projectName
3447
- });
4326
+ const content = replaceTemplateVars(file.content, { projectName });
3448
4327
  await writeFile(filePath, content);
3449
4328
  }
3450
4329
  }
3451
- spinner.text = "\u6B63\u5728\u751F\u6210 package.json...";
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
+ }
4339
+ spinner.text = "Generating package.json...";
3452
4340
  const pkgJson = generateBasePackageJson(projectName);
3453
4341
  const allDeps = {};
3454
4342
  const allDevDeps = {};
@@ -3463,48 +4351,302 @@ async function createProject(projectName) {
3463
4351
  );
3464
4352
  const pkgPath = import_path2.default.join(targetDir, "package.json");
3465
4353
  await writeJson(pkgPath, pkgJson);
3466
- spinner.text = "\u6B63\u5728\u914D\u7F6E app.json...";
4354
+ spinner.text = "Configuring app.json...";
3467
4355
  await updateAppJson(targetDir, selectedModuleDefs, projectName);
3468
- spinner.text = "\u6B63\u5728\u914D\u7F6E Babel...";
4356
+ spinner.text = "Configuring Babel...";
3469
4357
  await updateBabelConfig(targetDir, selectedModuleDefs);
3470
- spinner.text = "\u6B63\u5728\u914D\u7F6E\u5165\u53E3\u6587\u4EF6...";
4358
+ spinner.text = "Configuring layout...";
3471
4359
  await updateLayoutFile(targetDir, selectedModuleDefs);
3472
- spinner.text = "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56 (yarn install)...";
4360
+ await writeProjectConfig(targetDir, {
4361
+ projectName,
4362
+ selectedModules: finalModuleIds,
4363
+ cliVersion: CLI_VERSION,
4364
+ uiTemplate: isLoginTabs ? "login-tabs" : "default"
4365
+ });
4366
+ spinner.text = "Installing dependencies (yarn install)...";
3473
4367
  try {
3474
4368
  await (0, import_execa.execa)("yarn", ["install"], {
3475
4369
  cwd: targetDir,
3476
4370
  timeout: 3e5
3477
- // 5 minute timeout
3478
4371
  });
3479
4372
  } catch (installError) {
3480
4373
  const errMsg = installError instanceof Error ? installError.message : String(installError);
3481
- spinner.warn("yarn install \u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u5B89\u88C5\u4F9D\u8D56");
3482
- console.log(import_chalk.default.red(` \u9519\u8BEF: ${errMsg}`));
3483
- console.log(
3484
- import_chalk.default.gray(` cd ${projectName} && yarn install`)
3485
- );
4374
+ spinner.warn("yarn install failed, please install manually");
4375
+ console.log(import_chalk.default.red(` Error: ${errMsg}`));
4376
+ console.log(import_chalk.default.gray(` cd ${projectName} && yarn install`));
3486
4377
  }
3487
- spinner.succeed(import_chalk.default.green("\u9879\u76EE\u521B\u5EFA\u6210\u529F\uFF01"));
4378
+ spinner.succeed(import_chalk.default.green("Project created!"));
3488
4379
  console.log();
3489
- console.log(import_chalk.default.bold(" \u{1F389} \u4E0B\u4E00\u6B65\uFF1A"));
4380
+ console.log(import_chalk.default.bold(" \u{1F389} Next steps:"));
3490
4381
  console.log(import_chalk.default.white(` cd ${projectName}`));
3491
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
+ }
3492
4386
  console.log();
3493
4387
  if (selectedModuleDefs.length > 0) {
3494
- console.log(import_chalk.default.bold(" \u{1F4CB} \u5DF2\u9009\u6A21\u5757\uFF1A"));
4388
+ console.log(import_chalk.default.bold(" \u{1F4CB} Selected modules:"));
3495
4389
  for (const mod of selectedModuleDefs) {
3496
4390
  console.log(import_chalk.default.white(` \u2713 ${mod.name}`));
3497
4391
  }
3498
4392
  console.log();
3499
4393
  }
3500
4394
  } catch (error) {
3501
- spinner.fail(import_chalk.default.red("\u9879\u76EE\u521B\u5EFA\u5931\u8D25"));
4395
+ spinner.fail(import_chalk.default.red("Project creation failed"));
3502
4396
  console.error(error);
3503
4397
  process.exit(1);
3504
4398
  }
3505
4399
  }
3506
- async function updateAppJson(targetDir, selectedModules, projectName) {
4400
+ async function upgradeProject(targetDir) {
4401
+ console.log();
4402
+ console.log(
4403
+ import_chalk.default.bold.cyan(" \u2554\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\u2557")
4404
+ );
4405
+ console.log(
4406
+ import_chalk.default.bold.cyan(" \u2551 expo-bbase \u2014 Upgrade \u2551")
4407
+ );
4408
+ console.log(
4409
+ 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")
4410
+ );
4411
+ console.log();
4412
+ const absDir = import_path2.default.resolve(targetDir);
4413
+ const config = await readProjectConfig(absDir);
4414
+ if (!config) {
4415
+ console.error(
4416
+ import_chalk.default.red(
4417
+ ` \u2716 No ${CONFIG_FILE} found in ${absDir}
4418
+ This directory doesn't appear to be an expo-bbase project.
4419
+ If it is, run "expo-bbase add" to register modules.`
4420
+ )
4421
+ );
4422
+ process.exit(1);
4423
+ }
4424
+ console.log(
4425
+ import_chalk.default.white(` \u{1F4C2} Project: ${import_chalk.default.bold(config.projectName)}`)
4426
+ );
4427
+ console.log(
4428
+ import_chalk.default.white(` \u{1F4CB} CLI version: ${import_chalk.default.gray(config.cliVersion || "unknown")} \u2192 ${import_chalk.default.green(CLI_VERSION)}`)
4429
+ );
4430
+ console.log(
4431
+ import_chalk.default.white(
4432
+ ` \u{1F9E9} Modules: ${import_chalk.default.green(config.selectedModules.join(", ") || "none")}`
4433
+ )
4434
+ );
4435
+ console.log();
4436
+ const spinner = (0, import_ora.default)("Upgrading project...").start();
4437
+ try {
4438
+ const selectedModuleDefs = getModulesByIds(config.selectedModules);
4439
+ spinner.text = "Updating module files...";
4440
+ for (const mod of selectedModuleDefs) {
4441
+ for (const file of mod.files) {
4442
+ const filePath = import_path2.default.join(absDir, file.path);
4443
+ const content = replaceTemplateVars(file.content, {
4444
+ projectName: config.projectName
4445
+ });
4446
+ await writeFile(filePath, content);
4447
+ }
4448
+ }
4449
+ spinner.text = "Updating dependencies...";
4450
+ const allDeps = {};
4451
+ const allDevDeps = {};
4452
+ for (const mod of selectedModuleDefs) {
4453
+ Object.assign(allDeps, mod.dependencies);
4454
+ Object.assign(allDevDeps, mod.devDependencies);
4455
+ }
4456
+ const basePkg = generateBasePackageJson(config.projectName);
4457
+ Object.assign(allDeps, basePkg.dependencies);
4458
+ Object.assign(
4459
+ allDevDeps,
4460
+ basePkg.devDependencies
4461
+ );
4462
+ const pkgPath = import_path2.default.join(absDir, "package.json");
4463
+ await mergeDependencies(pkgPath, allDeps, allDevDeps);
4464
+ spinner.text = "Updating app.json...";
4465
+ await updateAppJson(absDir, selectedModuleDefs, config.projectName);
4466
+ spinner.text = "Updating Babel config...";
4467
+ await updateBabelConfig(absDir, selectedModuleDefs);
4468
+ spinner.text = "Updating layout...";
4469
+ await updateLayoutFile(absDir, selectedModuleDefs);
4470
+ config.cliVersion = CLI_VERSION;
4471
+ await writeProjectConfig(absDir, config);
4472
+ spinner.text = "Installing updated dependencies (yarn install)...";
4473
+ try {
4474
+ await (0, import_execa.execa)("yarn", ["install"], {
4475
+ cwd: absDir,
4476
+ timeout: 3e5
4477
+ });
4478
+ } catch (installError) {
4479
+ const errMsg = installError instanceof Error ? installError.message : String(installError);
4480
+ spinner.warn("yarn install failed, please install manually");
4481
+ console.log(import_chalk.default.red(` Error: ${errMsg}`));
4482
+ }
4483
+ spinner.succeed(import_chalk.default.green("Project upgraded!"));
4484
+ console.log();
4485
+ console.log(import_chalk.default.bold(" \u{1F4CB} Upgrade summary:"));
4486
+ console.log(import_chalk.default.white(` CLI: ${import_chalk.default.gray(config.cliVersion)} (before)`));
4487
+ console.log(import_chalk.default.white(` Modules: ${import_chalk.default.green(config.selectedModules.join(", "))}`));
4488
+ console.log();
4489
+ console.log(import_chalk.default.bold(" \u{1F389} Run your project:"));
4490
+ console.log(import_chalk.default.white(" npx expo start"));
4491
+ console.log();
4492
+ } catch (error) {
4493
+ spinner.fail(import_chalk.default.red("Upgrade failed"));
4494
+ console.error(error);
4495
+ process.exit(1);
4496
+ }
4497
+ }
4498
+ async function addModule(moduleIds, targetDir) {
4499
+ console.log();
4500
+ const absDir = import_path2.default.resolve(targetDir);
4501
+ let config = await readProjectConfig(absDir);
4502
+ if (!config) {
4503
+ const pkgPath = import_path2.default.join(absDir, "package.json");
4504
+ if (!await import_fs_extra2.default.pathExists(pkgPath)) {
4505
+ console.error(
4506
+ import_chalk.default.red(` \u2716 No package.json found in ${absDir}`)
4507
+ );
4508
+ process.exit(1);
4509
+ }
4510
+ const pkg = await import_fs_extra2.default.readJson(pkgPath);
4511
+ config = {
4512
+ projectName: pkg.name || import_path2.default.basename(absDir),
4513
+ selectedModules: [],
4514
+ cliVersion: CLI_VERSION
4515
+ };
4516
+ console.log(
4517
+ import_chalk.default.yellow(
4518
+ ` \u26A0 No ${CONFIG_FILE} found. Creating one for project "${config.projectName}".`
4519
+ )
4520
+ );
4521
+ }
4522
+ if (moduleIds.length === 0) {
4523
+ const availableModules = modules.filter(
4524
+ (m) => !config.selectedModules.includes(m.id)
4525
+ );
4526
+ if (availableModules.length === 0) {
4527
+ console.log(import_chalk.default.green(" \u2713 All modules are already installed!"));
4528
+ process.exit(0);
4529
+ }
4530
+ const choices = availableModules.map((m) => ({
4531
+ title: `${import_chalk.default.bold(m.name)} \u2014 ${import_chalk.default.gray(m.description)}`,
4532
+ value: m.id,
4533
+ selected: false
4534
+ }));
4535
+ const { selected } = await (0, import_prompts.default)({
4536
+ type: "multiselect",
4537
+ name: "selected",
4538
+ message: "Select modules to add (Space to toggle, Enter to confirm)",
4539
+ choices,
4540
+ hint: "- Space toggle \xB7 a select all/none \xB7 Enter confirm",
4541
+ instructions: false
4542
+ });
4543
+ if (selected === void 0 || selected.length === 0) {
4544
+ console.log(import_chalk.default.yellow(" No modules selected."));
4545
+ process.exit(0);
4546
+ }
4547
+ moduleIds = selected;
4548
+ }
4549
+ const invalidIds = moduleIds.filter((id) => !getModuleById(id));
4550
+ if (invalidIds.length > 0) {
4551
+ console.error(
4552
+ import_chalk.default.red(` \u2716 Unknown module(s): ${invalidIds.join(", ")}`)
4553
+ );
4554
+ console.log(
4555
+ import_chalk.default.gray(
4556
+ ` Available: ${modules.map((m) => m.id).join(", ")}`
4557
+ )
4558
+ );
4559
+ process.exit(1);
4560
+ }
4561
+ const newModuleIds = moduleIds.filter(
4562
+ (id) => !config.selectedModules.includes(id)
4563
+ );
4564
+ if (newModuleIds.length === 0) {
4565
+ console.log(
4566
+ import_chalk.default.yellow(" All specified modules are already installed.")
4567
+ );
4568
+ process.exit(0);
4569
+ }
4570
+ const newModuleDefs = getModulesByIds(newModuleIds);
4571
+ console.log(
4572
+ import_chalk.default.white(` \u{1F4C2} Project: ${import_chalk.default.bold(config.projectName)}`)
4573
+ );
4574
+ console.log(
4575
+ import_chalk.default.white(
4576
+ ` \u2795 Adding: ${import_chalk.default.green(newModuleDefs.map((m) => m.name).join(", "))}`
4577
+ )
4578
+ );
4579
+ console.log();
4580
+ const spinner = (0, import_ora.default)("Adding modules...").start();
4581
+ try {
4582
+ spinner.text = "Writing module files...";
4583
+ for (const mod of newModuleDefs) {
4584
+ for (const file of mod.files) {
4585
+ const filePath = import_path2.default.join(absDir, file.path);
4586
+ const content = replaceTemplateVars(file.content, {
4587
+ projectName: config.projectName
4588
+ });
4589
+ await writeFile(filePath, content);
4590
+ }
4591
+ }
4592
+ spinner.text = "Updating dependencies...";
4593
+ const allDeps = {};
4594
+ const allDevDeps = {};
4595
+ for (const mod of newModuleDefs) {
4596
+ Object.assign(allDeps, mod.dependencies);
4597
+ Object.assign(allDevDeps, mod.devDependencies);
4598
+ }
4599
+ const pkgPath = import_path2.default.join(absDir, "package.json");
4600
+ await mergeDependencies(pkgPath, allDeps, allDevDeps);
4601
+ spinner.text = "Updating app.json...";
4602
+ const existingModules = getModulesByIds(config.selectedModules);
4603
+ await updateAppJson(
4604
+ absDir,
4605
+ [...existingModules, ...newModuleDefs],
4606
+ config.projectName
4607
+ );
4608
+ spinner.text = "Updating Babel config...";
4609
+ await updateBabelConfig(absDir, newModuleDefs);
4610
+ spinner.text = "Updating layout...";
4611
+ await updateLayoutFile(absDir, newModuleDefs);
4612
+ config.selectedModules.push(...newModuleIds);
4613
+ config.cliVersion = CLI_VERSION;
4614
+ await writeProjectConfig(absDir, config);
4615
+ spinner.text = "Installing dependencies (yarn install)...";
4616
+ try {
4617
+ await (0, import_execa.execa)("yarn", ["install"], {
4618
+ cwd: absDir,
4619
+ timeout: 3e5
4620
+ });
4621
+ } catch (installError) {
4622
+ const errMsg = installError instanceof Error ? installError.message : String(installError);
4623
+ spinner.warn("yarn install failed, please install manually");
4624
+ console.log(import_chalk.default.red(` Error: ${errMsg}`));
4625
+ }
4626
+ spinner.succeed(import_chalk.default.green("Modules added!"));
4627
+ console.log();
4628
+ console.log(import_chalk.default.bold(" \u{1F4CB} Added modules:"));
4629
+ for (const mod of newModuleDefs) {
4630
+ console.log(import_chalk.default.white(` \u2713 ${mod.name} (${mod.id})`));
4631
+ }
4632
+ console.log();
4633
+ console.log(import_chalk.default.bold(" \u{1F9E9} All installed modules:"));
4634
+ for (const id of config.selectedModules) {
4635
+ const m = getModuleById(id);
4636
+ console.log(import_chalk.default.white(` \u2022 ${m?.name || id}`));
4637
+ }
4638
+ console.log();
4639
+ } catch (error) {
4640
+ spinner.fail(import_chalk.default.red("Failed to add modules"));
4641
+ console.error(error);
4642
+ process.exit(1);
4643
+ }
4644
+ }
4645
+ async function updateAppJson(targetDir, selectedModules, _projectName) {
3507
4646
  const appJsonPath = import_path2.default.join(targetDir, "app.json");
4647
+ if (!await import_fs_extra2.default.pathExists(appJsonPath)) {
4648
+ return;
4649
+ }
3508
4650
  const appJson = await import_fs_extra2.default.readJson(appJsonPath);
3509
4651
  const existingPlugins = appJson.expo?.plugins || [];
3510
4652
  for (const mod of selectedModules) {
@@ -3526,41 +4668,55 @@ async function updateAppJson(targetDir, selectedModules, projectName) {
3526
4668
  }
3527
4669
  async function updateBabelConfig(targetDir, selectedModules) {
3528
4670
  const babelPath = import_path2.default.join(targetDir, "babel.config.js");
4671
+ if (!await import_fs_extra2.default.pathExists(babelPath)) {
4672
+ return;
4673
+ }
3529
4674
  let content = await import_fs_extra2.default.readFile(babelPath, "utf-8");
3530
4675
  const extraPlugins = [];
3531
4676
  for (const mod of selectedModules) {
3532
- if (mod.babelPlugins) {
4677
+ if (mod.babelPlugins && mod.babelPlugins.length > 0) {
3533
4678
  extraPlugins.push(...mod.babelPlugins);
3534
4679
  }
3535
4680
  }
3536
4681
  if (extraPlugins.length > 0) {
3537
- const pluginStrings = extraPlugins.map((p) => ` "${p}"`).join(",\n");
3538
- content = content.replace(
3539
- /plugins:\s*\[([^\]]*)\]/,
3540
- `plugins: [$1${pluginStrings ? ",\n" + pluginStrings : ""}]`
3541
- );
3542
- await import_fs_extra2.default.writeFile(babelPath, content, "utf-8");
4682
+ const pluginsToAdd = extraPlugins.filter((p) => !content.includes(p));
4683
+ if (pluginsToAdd.length > 0) {
4684
+ const pluginStrings = pluginsToAdd.map((p) => ` "${p}"`).join(",\n");
4685
+ content = content.replace(
4686
+ /plugins:\s*\[([^\]]*)\]/,
4687
+ `plugins: [$1${pluginStrings ? ",\n" + pluginStrings : ""}]`
4688
+ );
4689
+ await import_fs_extra2.default.writeFile(babelPath, content, "utf-8");
4690
+ }
3543
4691
  }
3544
4692
  }
3545
4693
  async function updateLayoutFile(targetDir, selectedModules) {
3546
4694
  const layoutPath = import_path2.default.join(targetDir, "app/_layout.tsx");
3547
- const fs2 = import_fs_extra2.default;
3548
- let content = await fs2.readFile(layoutPath, "utf-8");
4695
+ if (!await import_fs_extra2.default.pathExists(layoutPath)) {
4696
+ return;
4697
+ }
4698
+ let content = await import_fs_extra2.default.readFile(layoutPath, "utf-8");
3549
4699
  const extraImports = [];
3550
4700
  const extraProviderPairs = [];
3551
4701
  for (const mod of selectedModules) {
3552
4702
  if (mod.layoutImports) {
3553
- extraImports.push(...mod.layoutImports);
4703
+ for (const imp of mod.layoutImports) {
4704
+ if (!content.includes(imp)) {
4705
+ extraImports.push(imp);
4706
+ }
4707
+ }
3554
4708
  }
3555
4709
  if (mod.layoutProviders) {
3556
4710
  for (const provider of mod.layoutProviders) {
3557
4711
  const match = provider.match(/^<(\w+)/);
3558
4712
  if (match) {
3559
4713
  const tagName = match[1];
3560
- extraProviderPairs.push({
3561
- open: ` ${provider}`,
3562
- close: ` </${tagName}>`
3563
- });
4714
+ if (!content.includes(`<${tagName}`)) {
4715
+ extraProviderPairs.push({
4716
+ open: ` ${provider}`,
4717
+ close: ` </${tagName}>`
4718
+ });
4719
+ }
3564
4720
  }
3565
4721
  }
3566
4722
  }
@@ -3589,7 +4745,7 @@ async function updateLayoutFile(targetDir, selectedModules) {
3589
4745
  }
3590
4746
  }
3591
4747
  }
3592
- await fs2.writeFile(layoutPath, content, "utf-8");
4748
+ await import_fs_extra2.default.writeFile(layoutPath, content, "utf-8");
3593
4749
  }
3594
4750
  run().catch((error) => {
3595
4751
  console.error(import_chalk.default.red("Fatal error:"), error);
@@ -3597,7 +4753,9 @@ run().catch((error) => {
3597
4753
  });
3598
4754
  // Annotate the CommonJS export names for ESM import in node:
3599
4755
  0 && (module.exports = {
4756
+ addModule,
3600
4757
  createProject,
3601
- run
4758
+ run,
4759
+ upgradeProject
3602
4760
  });
3603
4761
  //# sourceMappingURL=index.js.map