mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-18 18:31:20 +00:00
initialize project with @react-native-reusables/cli
This commit is contained in:
commit
08fc64a6a3
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal 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
16
README.md
Normal 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
40
app.json
Normal 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
18
app/+not-found.tsx
Normal 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
69
app/_layout.tsx
Normal 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
95
app/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
BIN
assets/images/adaptive-icon.png
Normal file
BIN
assets/images/adaptive-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
assets/images/favicon.png
Normal file
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
BIN
assets/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/splash.png
Normal file
BIN
assets/images/splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
6
babel.config.js
Normal file
6
babel.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
|
||||
};
|
||||
};
|
38
components/ThemeToggle.tsx
Normal file
38
components/ThemeToggle.tsx
Normal 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
45
components/ui/avatar.tsx
Normal 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
88
components/ui/button.tsx
Normal 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
57
components/ui/card.tsx
Normal 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 };
|
61
components/ui/progress.tsx
Normal file
61
components/ui/progress.tsx
Normal 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
24
components/ui/text.tsx
Normal 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
39
components/ui/tooltip.tsx
Normal 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
49
global.css
Normal 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
12
index.js
Normal 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);
|
11
lib/android-navigation-bar.ts
Normal file
11
lib/android-navigation-bar.ts
Normal 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
18
lib/constants.ts
Normal 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
4
lib/icons/Info.tsx
Normal 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
4
lib/icons/MoonStar.tsx
Normal 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
4
lib/icons/Sun.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { Sun } from 'lucide-react-native';
|
||||
import { iconWithClassName } from './iconWithClassName';
|
||||
iconWithClassName(Sun);
|
||||
export { Sun };
|
14
lib/icons/iconWithClassName.ts
Normal file
14
lib/icons/iconWithClassName.ts
Normal 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
11
lib/useColorScheme.tsx
Normal 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
6
lib/utils.ts
Normal 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
6
metro.config.js
Normal 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
1
nativewind-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="nativewind/types" />
|
13443
package-lock.json
generated
Normal file
13443
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
package.json
Normal file
53
package.json
Normal 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
65
tailwind.config.js
Normal 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
19
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user