initialize project with @react-native-reusables/cli

This commit is contained in:
DocNR 2025-02-05 20:38:39 -05:00
commit 08fc64a6a3
34 changed files with 14357 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
# The following patterns were generated by expo-cli
expo-env.d.ts
# @end expo-cli

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# Starter base
A starting point to help you set up your project quickly and use the common components provided by `react-native-reusables`. The idea is to make it easier for you to get started.
## Features
- NativeWind v4
- Dark and light mode
- Android Navigation Bar matches mode
- Persistent mode
- Common components
- ThemeToggle, Avatar, Button, Card, Progress, Text, Tooltip
<img src="https://github.com/mrzachnugent/react-native-reusables/assets/63797719/42c94108-38a7-498b-9c70-18640420f1bc"
alt="starter-base-template"
style="width:270px;" />

40
app.json Normal file
View File

@ -0,0 +1,40 @@
{
"expo": {
"name": "rnr-test",
"slug": "rnr-test",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router"
],
"experiments": {
"typedRoutes": true
}
}
}

18
app/+not-found.tsx Normal file
View File

@ -0,0 +1,18 @@
import { Link, Stack } from 'expo-router';
import { View } from 'react-native';
import { Text } from '~/components/ui/text';
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<View>
<Text>This screen doesn't exist.</Text>
<Link href='/'>
<Text>Go to home screen!</Text>
</Link>
</View>
</>
);
}

69
app/_layout.tsx Normal file
View File

@ -0,0 +1,69 @@
import '~/global.css';
import { DarkTheme, DefaultTheme, Theme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import * as React from 'react';
import { Platform } from 'react-native';
import { NAV_THEME } from '~/lib/constants';
import { useColorScheme } from '~/lib/useColorScheme';
import { PortalHost } from '@rn-primitives/portal';
import { ThemeToggle } from '~/components/ThemeToggle';
import { setAndroidNavigationBar } from '~/lib/android-navigation-bar';
const LIGHT_THEME: Theme = {
...DefaultTheme,
colors: NAV_THEME.light,
};
const DARK_THEME: Theme = {
...DarkTheme,
colors: NAV_THEME.dark,
};
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from 'expo-router';
export default function RootLayout() {
const hasMounted = React.useRef(false);
const { colorScheme, isDarkColorScheme } = useColorScheme();
const [isColorSchemeLoaded, setIsColorSchemeLoaded] = React.useState(false);
useIsomorphicLayoutEffect(() => {
if (hasMounted.current) {
return;
}
if (Platform.OS === 'web') {
// Adds the background color to the html element to prevent white background on overscroll.
document.documentElement.classList.add('bg-background');
}
setAndroidNavigationBar(colorScheme);
setIsColorSchemeLoaded(true);
hasMounted.current = true;
}, []);
if (!isColorSchemeLoaded) {
return null;
}
return (
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
<StatusBar style={isDarkColorScheme ? 'light' : 'dark'} />
<Stack>
<Stack.Screen
name='index'
options={{
title: 'Starter Base',
headerRight: () => <ThemeToggle />,
}}
/>
</Stack>
<PortalHost />
</ThemeProvider>
);
}
const useIsomorphicLayoutEffect =
Platform.OS === 'web' && typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect;

95
app/index.tsx Normal file
View File

@ -0,0 +1,95 @@
import * as React from 'react';
import { View } from 'react-native';
import Animated, { FadeInUp, FadeOutDown, LayoutAnimationConfig } from 'react-native-reanimated';
import { Info } from '~/lib/icons/Info';
import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar';
import { Button } from '~/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '~/components/ui/card';
import { Progress } from '~/components/ui/progress';
import { Text } from '~/components/ui/text';
import { Tooltip, TooltipContent, TooltipTrigger } from '~/components/ui/tooltip';
const GITHUB_AVATAR_URI =
'https://i.pinimg.com/originals/ef/a2/8d/efa28d18a04e7fa40ed49eeb0ab660db.jpg';
export default function Screen() {
const [progress, setProgress] = React.useState(78);
function updateProgressValue() {
setProgress(Math.floor(Math.random() * 100));
}
return (
<View className='flex-1 justify-center items-center gap-5 p-6 bg-secondary/30'>
<Card className='w-full max-w-sm p-6 rounded-2xl'>
<CardHeader className='items-center'>
<Avatar alt="Rick Sanchez's Avatar" className='w-24 h-24'>
<AvatarImage source={{ uri: GITHUB_AVATAR_URI }} />
<AvatarFallback>
<Text>RS</Text>
</AvatarFallback>
</Avatar>
<View className='p-3' />
<CardTitle className='pb-2 text-center'>Rick Sanchez</CardTitle>
<View className='flex-row'>
<CardDescription className='text-base font-semibold'>Scientist</CardDescription>
<Tooltip delayDuration={150}>
<TooltipTrigger className='px-2 pb-0.5 active:opacity-50'>
<Info size={14} strokeWidth={2.5} className='w-4 h-4 text-foreground/70' />
</TooltipTrigger>
<TooltipContent className='py-2 px-4 shadow'>
<Text className='native:text-lg'>Freelance</Text>
</TooltipContent>
</Tooltip>
</View>
</CardHeader>
<CardContent>
<View className='flex-row justify-around gap-3'>
<View className='items-center'>
<Text className='text-sm text-muted-foreground'>Dimension</Text>
<Text className='text-xl font-semibold'>C-137</Text>
</View>
<View className='items-center'>
<Text className='text-sm text-muted-foreground'>Age</Text>
<Text className='text-xl font-semibold'>70</Text>
</View>
<View className='items-center'>
<Text className='text-sm text-muted-foreground'>Species</Text>
<Text className='text-xl font-semibold'>Human</Text>
</View>
</View>
</CardContent>
<CardFooter className='flex-col gap-3 pb-0'>
<View className='flex-row items-center overflow-hidden'>
<Text className='text-sm text-muted-foreground'>Productivity:</Text>
<LayoutAnimationConfig skipEntering>
<Animated.View
key={progress}
entering={FadeInUp}
exiting={FadeOutDown}
className='w-11 items-center'
>
<Text className='text-sm font-bold text-sky-600'>{progress}%</Text>
</Animated.View>
</LayoutAnimationConfig>
</View>
<Progress value={progress} className='h-2' indicatorClassName='bg-sky-600' />
<View />
<Button
variant='outline'
className='shadow shadow-foreground/5'
onPress={updateProgressValue}
>
<Text>Update</Text>
</Button>
</CardFooter>
</Card>
</View>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/images/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

6
babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
};
};

View File

@ -0,0 +1,38 @@
import { Pressable, View } from 'react-native';
import { setAndroidNavigationBar } from '~/lib/android-navigation-bar';
import { MoonStar } from '~/lib/icons/MoonStar';
import { Sun } from '~/lib/icons/Sun';
import { useColorScheme } from '~/lib/useColorScheme';
import { cn } from '~/lib/utils';
export function ThemeToggle() {
const { isDarkColorScheme, setColorScheme } = useColorScheme();
function toggleColorScheme() {
const newTheme = isDarkColorScheme ? 'light' : 'dark';
setColorScheme(newTheme);
setAndroidNavigationBar(newTheme);
}
return (
<Pressable
onPress={toggleColorScheme}
className='web:ring-offset-background web:transition-colors web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2'
>
{({ pressed }) => (
<View
className={cn(
'flex-1 aspect-square pt-0.5 justify-center items-start web:px-5',
pressed && 'opacity-70'
)}
>
{isDarkColorScheme ? (
<MoonStar className='text-foreground' size={23} strokeWidth={1.25} />
) : (
<Sun className='text-foreground' size={24} strokeWidth={1.25} />
)}
</View>
)}
</Pressable>
);
}

45
components/ui/avatar.tsx Normal file
View File

@ -0,0 +1,45 @@
import * as AvatarPrimitive from '@rn-primitives/avatar';
import * as React from 'react';
import { cn } from '~/lib/utils';
const AvatarPrimitiveRoot = AvatarPrimitive.Root;
const AvatarPrimitiveImage = AvatarPrimitive.Image;
const AvatarPrimitiveFallback = AvatarPrimitive.Fallback;
const Avatar = React.forwardRef<AvatarPrimitive.RootRef, AvatarPrimitive.RootProps>(
({ className, ...props }, ref) => (
<AvatarPrimitiveRoot
ref={ref}
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
{...props}
/>
)
);
Avatar.displayName = AvatarPrimitiveRoot.displayName;
const AvatarImage = React.forwardRef<AvatarPrimitive.ImageRef, AvatarPrimitive.ImageProps>(
({ className, ...props }, ref) => (
<AvatarPrimitiveImage
ref={ref}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
)
);
AvatarImage.displayName = AvatarPrimitiveImage.displayName;
const AvatarFallback = React.forwardRef<AvatarPrimitive.FallbackRef, AvatarPrimitive.FallbackProps>(
({ className, ...props }, ref) => (
<AvatarPrimitiveFallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted',
className
)}
{...props}
/>
)
);
AvatarFallback.displayName = AvatarPrimitiveFallback.displayName;
export { Avatar, AvatarFallback, AvatarImage };

88
components/ui/button.tsx Normal file
View File

@ -0,0 +1,88 @@
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { Pressable } from 'react-native';
import { TextClassContext } from '~/components/ui/text';
import { cn } from '~/lib/utils';
const buttonVariants = cva(
'group flex items-center justify-center rounded-md web:ring-offset-background web:transition-colors web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2',
{
variants: {
variant: {
default: 'bg-primary web:hover:opacity-90 active:opacity-90',
destructive: 'bg-destructive web:hover:opacity-90 active:opacity-90',
outline:
'border border-input bg-background web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent',
secondary: 'bg-secondary web:hover:opacity-80 active:opacity-80',
ghost: 'web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent',
link: 'web:underline-offset-4 web:hover:underline web:focus:underline ',
},
size: {
default: 'h-10 px-4 py-2 native:h-12 native:px-5 native:py-3',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8 native:h-14',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const buttonTextVariants = cva(
'web:whitespace-nowrap text-sm native:text-base font-medium text-foreground web:transition-colors',
{
variants: {
variant: {
default: 'text-primary-foreground',
destructive: 'text-destructive-foreground',
outline: 'group-active:text-accent-foreground',
secondary: 'text-secondary-foreground group-active:text-secondary-foreground',
ghost: 'group-active:text-accent-foreground',
link: 'text-primary group-active:underline',
},
size: {
default: '',
sm: '',
lg: 'native:text-lg',
icon: '',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
type ButtonProps = React.ComponentPropsWithoutRef<typeof Pressable> &
VariantProps<typeof buttonVariants>;
const Button = React.forwardRef<React.ElementRef<typeof Pressable>, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<TextClassContext.Provider
value={cn(
props.disabled && 'web:pointer-events-none',
buttonTextVariants({ variant, size })
)}
>
<Pressable
className={cn(
props.disabled && 'opacity-50 web:pointer-events-none',
buttonVariants({ variant, size, className })
)}
ref={ref}
role='button'
{...props}
/>
</TextClassContext.Provider>
);
}
);
Button.displayName = 'Button';
export { Button, buttonTextVariants, buttonVariants };
export type { ButtonProps };

57
components/ui/card.tsx Normal file
View File

@ -0,0 +1,57 @@
import type { TextRef, ViewRef } from '@rn-primitives/types';
import * as React from 'react';
import { Text, TextProps, View, ViewProps } from 'react-native';
import { TextClassContext } from '~/components/ui/text';
import { cn } from '~/lib/utils';
const Card = React.forwardRef<ViewRef, ViewProps>(({ className, ...props }, ref) => (
<View
ref={ref}
className={cn(
'rounded-lg border border-border bg-card shadow-sm shadow-foreground/10',
className
)}
{...props}
/>
));
Card.displayName = 'Card';
const CardHeader = React.forwardRef<ViewRef, ViewProps>(({ className, ...props }, ref) => (
<View ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
));
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<TextRef, React.ComponentPropsWithoutRef<typeof Text>>(
({ className, ...props }, ref) => (
<Text
role='heading'
aria-level={3}
ref={ref}
className={cn(
'text-2xl text-card-foreground font-semibold leading-none tracking-tight',
className
)}
{...props}
/>
)
);
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<TextRef, TextProps>(({ className, ...props }, ref) => (
<Text ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<ViewRef, ViewProps>(({ className, ...props }, ref) => (
<TextClassContext.Provider value='text-card-foreground'>
<View ref={ref} className={cn('p-6 pt-0', className)} {...props} />
</TextClassContext.Provider>
));
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<ViewRef, ViewProps>(({ className, ...props }, ref) => (
<View ref={ref} className={cn('flex flex-row items-center p-6 pt-0', className)} {...props} />
));
CardFooter.displayName = 'CardFooter';
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };

View File

@ -0,0 +1,61 @@
import * as ProgressPrimitive from '@rn-primitives/progress';
import * as React from 'react';
import { Platform, View } from 'react-native';
import Animated, {
Extrapolation,
interpolate,
useAnimatedStyle,
useDerivedValue,
withSpring,
} from 'react-native-reanimated';
import { cn } from '~/lib/utils';
const Progress = React.forwardRef<
ProgressPrimitive.RootRef,
ProgressPrimitive.RootProps & {
indicatorClassName?: string;
}
>(({ className, value, indicatorClassName, ...props }, ref) => {
return (
<ProgressPrimitive.Root
ref={ref}
className={cn('relative h-4 w-full overflow-hidden rounded-full bg-secondary', className)}
{...props}
>
<Indicator value={value} className={indicatorClassName} />
</ProgressPrimitive.Root>
);
});
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };
function Indicator({ value, className }: { value: number | undefined | null; className?: string }) {
const progress = useDerivedValue(() => value ?? 0);
const indicator = useAnimatedStyle(() => {
return {
width: withSpring(
`${interpolate(progress.value, [0, 100], [1, 100], Extrapolation.CLAMP)}%`,
{ overshootClamping: true }
),
};
});
if (Platform.OS === 'web') {
return (
<View
className={cn('h-full w-full flex-1 bg-primary web:transition-all', className)}
style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
>
<ProgressPrimitive.Indicator className={cn('h-full w-full ', className)} />
</View>
);
}
return (
<ProgressPrimitive.Indicator asChild>
<Animated.View style={indicator} className={cn('h-full bg-foreground', className)} />
</ProgressPrimitive.Indicator>
);
}

24
components/ui/text.tsx Normal file
View File

@ -0,0 +1,24 @@
import * as Slot from '@rn-primitives/slot';
import type { SlottableTextProps, TextRef } from '@rn-primitives/types';
import * as React from 'react';
import { Text as RNText } from 'react-native';
import { cn } from '~/lib/utils';
const TextClassContext = React.createContext<string | undefined>(undefined);
const Text = React.forwardRef<TextRef, SlottableTextProps>(
({ className, asChild = false, ...props }, ref) => {
const textClass = React.useContext(TextClassContext);
const Component = asChild ? Slot.Text : RNText;
return (
<Component
className={cn('text-base text-foreground web:select-text', textClass, className)}
ref={ref}
{...props}
/>
);
}
);
Text.displayName = 'Text';
export { Text, TextClassContext };

39
components/ui/tooltip.tsx Normal file
View File

@ -0,0 +1,39 @@
import * as TooltipPrimitive from '@rn-primitives/tooltip';
import * as React from 'react';
import { Platform, StyleSheet } from 'react-native';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { TextClassContext } from '~/components/ui/text';
import { cn } from '~/lib/utils';
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
TooltipPrimitive.ContentRef,
TooltipPrimitive.ContentProps & { portalHost?: string }
>(({ className, sideOffset = 4, portalHost, ...props }, ref) => (
<TooltipPrimitive.Portal hostName={portalHost}>
<TooltipPrimitive.Overlay style={Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined}>
<Animated.View
entering={Platform.select({ web: undefined, default: FadeIn })}
exiting={Platform.select({ web: undefined, default: FadeOut })}
>
<TextClassContext.Provider value='text-sm native:text-base text-popover-foreground'>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border border-border bg-popover px-3 py-1.5 shadow-md shadow-foreground/5 web:animate-in web:fade-in-0 web:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</TextClassContext.Provider>
</Animated.View>
</TooltipPrimitive.Overlay>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipContent, TooltipTrigger };

49
global.css Normal file
View File

@ -0,0 +1,49 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
}
.dark:root {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}

12
index.js Normal file
View File

@ -0,0 +1,12 @@
import { registerRootComponent } from 'expo';
import { ExpoRoot } from 'expo-router';
// https://docs.expo.dev/router/reference/troubleshooting/#expo_router_app_root-not-defined
// Must be exported or Fast Refresh won't update the context
export function App() {
const ctx = require.context('./app');
return <ExpoRoot context={ctx} />;
}
registerRootComponent(App);

View File

@ -0,0 +1,11 @@
import * as NavigationBar from 'expo-navigation-bar';
import { Platform } from 'react-native';
import { NAV_THEME } from '~/lib/constants';
export async function setAndroidNavigationBar(theme: 'light' | 'dark') {
if (Platform.OS !== 'android') return;
await NavigationBar.setButtonStyleAsync(theme === 'dark' ? 'light' : 'dark');
await NavigationBar.setBackgroundColorAsync(
theme === 'dark' ? NAV_THEME.dark.background : NAV_THEME.light.background
);
}

18
lib/constants.ts Normal file
View File

@ -0,0 +1,18 @@
export const NAV_THEME = {
light: {
background: 'hsl(0 0% 100%)', // background
border: 'hsl(240 5.9% 90%)', // border
card: 'hsl(0 0% 100%)', // card
notification: 'hsl(0 84.2% 60.2%)', // destructive
primary: 'hsl(240 5.9% 10%)', // primary
text: 'hsl(240 10% 3.9%)', // foreground
},
dark: {
background: 'hsl(240 10% 3.9%)', // background
border: 'hsl(240 3.7% 15.9%)', // border
card: 'hsl(240 10% 3.9%)', // card
notification: 'hsl(0 72% 51%)', // destructive
primary: 'hsl(0 0% 98%)', // primary
text: 'hsl(0 0% 98%)', // foreground
},
};

4
lib/icons/Info.tsx Normal file
View File

@ -0,0 +1,4 @@
import { Info } from 'lucide-react-native';
import { iconWithClassName } from './iconWithClassName';
iconWithClassName(Info);
export { Info };

4
lib/icons/MoonStar.tsx Normal file
View File

@ -0,0 +1,4 @@
import { MoonStar } from 'lucide-react-native';
import { iconWithClassName } from './iconWithClassName';
iconWithClassName(MoonStar);
export { MoonStar };

4
lib/icons/Sun.tsx Normal file
View File

@ -0,0 +1,4 @@
import { Sun } from 'lucide-react-native';
import { iconWithClassName } from './iconWithClassName';
iconWithClassName(Sun);
export { Sun };

View File

@ -0,0 +1,14 @@
import type { LucideIcon } from 'lucide-react-native';
import { cssInterop } from 'nativewind';
export function iconWithClassName(icon: LucideIcon) {
cssInterop(icon, {
className: {
target: 'style',
nativeStyleToProp: {
color: true,
opacity: true,
},
},
});
}

11
lib/useColorScheme.tsx Normal file
View File

@ -0,0 +1,11 @@
import { useColorScheme as useNativewindColorScheme } from 'nativewind';
export function useColorScheme() {
const { colorScheme, setColorScheme, toggleColorScheme } = useNativewindColorScheme();
return {
colorScheme: colorScheme ?? 'dark',
isDarkColorScheme: colorScheme === 'dark',
setColorScheme,
toggleColorScheme,
};
}

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

6
metro.config.js Normal file
View File

@ -0,0 +1,6 @@
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: './global.css' });

1
nativewind-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="nativewind/types" />

13443
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "rnr-test",
"main": "index.js",
"version": "1.0.0",
"scripts": {
"dev": "expo start -c",
"dev:web": "expo start -c --web",
"dev:android": "expo start -c --android",
"android": "expo start -c --android",
"ios": "expo start -c --ios",
"web": "expo start -c --web",
"clean": "rm -rf .expo node_modules",
"postinstall": "npx tailwindcss -i ./global.css -o ./node_modules/.cache/nativewind/global.css"
},
"dependencies": {
"@react-navigation/native": "^7.0.0",
"@rn-primitives/avatar": "~1.1.0",
"@rn-primitives/portal": "~1.1.0",
"@rn-primitives/progress": "~1.1.0",
"@rn-primitives/slot": "~1.1.0",
"@rn-primitives/tooltip": "~1.1.0",
"@rn-primitives/types": "~1.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"expo": "^52.0.25",
"expo-linking": "~7.0.4",
"expo-navigation-bar": "~4.0.7",
"expo-router": "~4.0.16",
"expo-splash-screen": "~0.29.20",
"expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.7",
"lucide-react-native": "^0.378.0",
"nativewind": "^4.1.23",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.6",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-web": "~0.19.13",
"tailwind-merge": "^2.2.1",
"tailwindcss": "3.3.5",
"tailwindcss-animate": "^1.0.7",
"zustand": "^4.4.7"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@types/react": "~18.3.12",
"typescript": "^5.3.3"
},
"private": true
}

65
tailwind.config.js Normal file
View File

@ -0,0 +1,65 @@
const { hairlineWidth } = require('nativewind/theme');
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
presets: [require('nativewind/preset')],
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderWidth: {
hairline: hairlineWidth(),
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [require('tailwindcss-animate')],
};

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"paths": {
"~/*": [
"*"
]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts",
"nativewind-env.d.ts"
]
}