diff --git a/with-router-rnr/README.md b/with-router-rnr/README.md new file mode 100644 index 00000000..4a3d27c5 --- /dev/null +++ b/with-router-rnr/README.md @@ -0,0 +1,18 @@ +# Expo Router and React Native Reusables + +Use [Expo Router](https://docs.expo.dev/router/introduction/) with [React Native Reusables](https://github.com/mrzachnugent/react-native-reusables/tree/main) components. This works like a universal shadcn/ui, but for all platforms. It leverages nativewind for styling and combines a number of community libraries to provide a consistent experience across web, iOS, and Android. + +## 🚀 How to use + +```sh +npx create-expo-app -e with-router-rnr +``` + +Read the RNR docs to learn [how to add components](https://www.reactnativereusables.com/getting-started/initial-setup/). + +## Deploy + +Deploy on all platforms with Expo Application Services (EAS). + +- Deploy the website: `npx eas-cli deploy` — [Learn more](https://docs.expo.dev/eas/hosting/get-started/) +- Deploy on iOS and Android using: `npx eas-cli build` — [Learn more](https://expo.dev/eas) diff --git a/with-router-rnr/app.json b/with-router-rnr/app.json new file mode 100644 index 00000000..55db85cb --- /dev/null +++ b/with-router-rnr/app.json @@ -0,0 +1,11 @@ +{ + "expo": { + "scheme": "acme", + "userInterfaceStyle": "automatic", + "orientation": "default", + "web": { + "output": "static" + }, + "plugins": ["expo-router"] + } +} diff --git a/with-router-rnr/babel.config.js b/with-router-rnr/babel.config.js new file mode 100644 index 00000000..f3c649bb --- /dev/null +++ b/with-router-rnr/babel.config.js @@ -0,0 +1,9 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: [ + ["babel-preset-expo", { jsxImportSource: "nativewind" }], + "nativewind/babel", + ], + }; +}; diff --git a/with-router-rnr/global.d.ts b/with-router-rnr/global.d.ts new file mode 100644 index 00000000..a13e3136 --- /dev/null +++ b/with-router-rnr/global.d.ts @@ -0,0 +1 @@ +/// diff --git a/with-router-rnr/metro.config.js b/with-router-rnr/metro.config.js new file mode 100644 index 00000000..34bf631d --- /dev/null +++ b/with-router-rnr/metro.config.js @@ -0,0 +1,6 @@ +const { getDefaultConfig } = require("expo/metro-config"); +const { withNativeWind } = require("nativewind/metro"); + +const config = getDefaultConfig(__dirname); + +module.exports = withNativeWind(config, { input: "./src/global.css" }); diff --git a/with-router-rnr/nativewind-env.d.ts b/with-router-rnr/nativewind-env.d.ts new file mode 100644 index 00000000..c0d83807 --- /dev/null +++ b/with-router-rnr/nativewind-env.d.ts @@ -0,0 +1,3 @@ +/// + +// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind. \ No newline at end of file diff --git a/with-router-rnr/package.json b/with-router-rnr/package.json new file mode 100644 index 00000000..e2e4256b --- /dev/null +++ b/with-router-rnr/package.json @@ -0,0 +1,64 @@ +{ + "name": "with-router-rnr", + "version": "1.0.0", + "main": "expo-router/entry", + "scripts": { + "start": "expo start", + "deploy": "npx expo export -p web && npx eas-cli@latest deploy" + }, + "dependencies": { + "@bacons/apple-colors": "^0.0.8", + "@gorhom/bottom-sheet": "^5.1.4", + "@hookform/resolvers": "^3.3.4", + "@react-native-community/slider": "4.5.6", + "@react-navigation/material-top-tabs": "^7.0.0", + "@react-navigation/native": "^7.0.0", + "@rn-primitives/aspect-ratio": "~1.2.0", + "@rn-primitives/avatar": "~1.2.0", + "@rn-primitives/collapsible": "~1.2.0", + "@rn-primitives/label": "~1.2.0", + "@rn-primitives/portal": "~1.3.0", + "@rn-primitives/progress": "~1.2.0", + "@rn-primitives/slider": "~1.2.0", + "@rn-primitives/toast": "~1.2.0", + "@rn-primitives/toolbar": "~1.2.0", + "@rn-primitives/tooltip": "~1.2.0", + "@shopify/flash-list": "1.7.6", + "@tanstack/react-table": "^8.11.7", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "expo": "^53.0.9", + "expo-font": "~13.3.1", + "expo-haptics": "~14.1.4", + "expo-linking": "~7.1.5", + "expo-navigation-bar": "^4.2.6", + "expo-router": "~5.0.7", + "expo-splash-screen": "~0.30.8", + "expo-status-bar": "~2.2.3", + "expo-system-ui": "~5.0.7", + "lucide-react-native": "^0.511.0", + "nativewind": "^4.1.23", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-hook-form": "^7.49.2", + "react-native": "0.79.2", + "react-native-calendars": "^1.1302.0", + "react-native-gesture-handler": "~2.24.0", + "react-native-reanimated": "~3.17.5", + "react-native-safe-area-context": "5.4.0", + "react-native-screens": "^4.10.0", + "react-native-svg": "15.11.2", + "react-native-tab-view": "^3.5.2", + "react-native-toast-message": "^2.2.0", + "react-native-web": "~0.20.0", + "tailwind-merge": "^2.2.1", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" + }, + "devDependencies": { + "@babel/core": "^7.20.0", + "@types/react": "~19.0.10", + "tailwindcss": "3.3.5", + "typescript": "~5.8.3" + } +} diff --git a/with-router-rnr/src/app/_layout.tsx b/with-router-rnr/src/app/_layout.tsx new file mode 100644 index 00000000..26f197f0 --- /dev/null +++ b/with-router-rnr/src/app/_layout.tsx @@ -0,0 +1,68 @@ +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 { NAV_THEME } from "@/lib/constants"; +import { useColorScheme } from "@/lib/useColorScheme"; +import { PortalHost } from "@rn-primitives/portal"; +import { ThemeToggle } from "@/components/ThemeToggle"; +import { useGlobals } from "@/lib/useGlobals"; +import { NativeStackNavigationOptions } from "@react-navigation/native-stack"; + +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"; + +// These are the default stack options for iOS, they disable on other platforms. +const DEFAULT_STACK_HEADER: NativeStackNavigationOptions = + process.env.EXPO_OS !== "ios" + ? {} + : { + headerTransparent: true, + headerBlurEffect: "systemChromeMaterial", + headerShadowVisible: true, + headerLargeTitleShadowVisible: false, + headerLargeStyle: { + backgroundColor: "transparent", + }, + headerLargeTitle: true, + }; + +export default function RootLayout() { + useGlobals(); + + const { isDarkColorScheme } = useColorScheme(); + + return ( + + + + , + }} + /> + + + + ); +} diff --git a/with-router-rnr/src/app/index.tsx b/with-router-rnr/src/app/index.tsx new file mode 100644 index 00000000..17782ba5 --- /dev/null +++ b/with-router-rnr/src/app/index.tsx @@ -0,0 +1,156 @@ +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"; +import { BodyScrollView } from "@/components/ui/body"; + +const GITHUB_AVATAR_URI = "https://github.com/expo.png"; + +export default function Screen() { + const [progress, setProgress] = React.useState(78); + + function updateProgressValue() { + setProgress(Math.floor(Math.random() * 100)); + } + return ( + + + + + + + RS + + + + Rick Sanchez + + + Scientist + + + + + + + Freelance + + + + + + + + Dimension + C-137 + + + Age + 70 + + + Species + Human + + + + + + Productivity: + + + + {progress}% + + + + + + + + + + + + + Your Orders + + Introducing Our Dynamic Orders Dashboard for Seamless Management and + Insightful Analysis. + + + + + + + + + + + This Month + + $5,329 + + + + +10% from last month + + + + + + + + + ); +} diff --git a/with-router-rnr/src/components/ThemeToggle.tsx b/with-router-rnr/src/components/ThemeToggle.tsx new file mode 100644 index 00000000..ce38913b --- /dev/null +++ b/with-router-rnr/src/components/ThemeToggle.tsx @@ -0,0 +1,30 @@ +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"; + +export function ThemeToggle() { + const { isDarkColorScheme, setColorScheme } = useColorScheme(); + + function toggleColorScheme() { + const newTheme = isDarkColorScheme ? "light" : "dark"; + setColorScheme(newTheme); + setAndroidNavigationBar(newTheme); + } + + return ( + + + {isDarkColorScheme ? ( + + ) : ( + + )} + + + ); +} diff --git a/with-router-rnr/src/components/ui/avatar.tsx b/with-router-rnr/src/components/ui/avatar.tsx new file mode 100644 index 00000000..bff7cac0 --- /dev/null +++ b/with-router-rnr/src/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +import * as AvatarPrimitive from "@rn-primitives/avatar"; +import * as React from "react"; +import { cn } from "@/lib/utils"; + +function Avatar({ + className, + ...props +}: AvatarPrimitive.RootProps & { + ref?: React.RefObject; +}) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: AvatarPrimitive.ImageProps & { + ref?: React.RefObject; +}) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: AvatarPrimitive.FallbackProps & { + ref?: React.RefObject; +}) { + return ( + + ); +} + +export { Avatar, AvatarFallback, AvatarImage }; diff --git a/with-router-rnr/src/components/ui/body.tsx b/with-router-rnr/src/components/ui/body.tsx new file mode 100644 index 00000000..31382e4f --- /dev/null +++ b/with-router-rnr/src/components/ui/body.tsx @@ -0,0 +1,15 @@ +import { ScrollViewProps } from "react-native"; +import Animated from "react-native-reanimated"; + +export function BodyScrollView( + props: ScrollViewProps & { ref?: React.Ref } +) { + return ( + + ); +} diff --git a/with-router-rnr/src/components/ui/button.tsx b/with-router-rnr/src/components/ui/button.tsx new file mode 100644 index 00000000..7c533c55 --- /dev/null +++ b/with-router-rnr/src/components/ui/button.tsx @@ -0,0 +1,112 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; +import { Pressable, Text } 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.ComponentProps & + VariantProps; + +function Button({ + ref, + className, + variant, + size, + children, + ...props +}: ButtonProps) { + return ( + + + {wrapChildren(children)} + + + ); +} + +import * as AC from "@bacons/apple-colors"; + +function wrapChildren(children: T): T | React.ReactNode { + if (children instanceof Function) { + return children; + } + return React.Children.map(children, (child) => { + if (typeof child === "string" || typeof child === "number") { + return {child}; + } + + return child; + }); +} + +export { Button, buttonTextVariants, buttonVariants }; +export type { ButtonProps }; diff --git a/with-router-rnr/src/components/ui/card.tsx b/with-router-rnr/src/components/ui/card.tsx new file mode 100644 index 00000000..3c14a2c5 --- /dev/null +++ b/with-router-rnr/src/components/ui/card.tsx @@ -0,0 +1,104 @@ +import * as React from "react"; +import { Text, TextProps, View, ViewProps } from "react-native"; +import { TextClassContext } from "@/components/ui/text"; +import { cn } from "@/lib/utils"; + +function Card({ + className, + ...props +}: ViewProps & { + ref?: React.RefObject; +}) { + return ( + + ); +} + +function CardHeader({ + className, + ...props +}: ViewProps & { + ref?: React.RefObject; +}) { + return ( + + ); +} + +function CardTitle({ + className, + ...props +}: TextProps & { + ref?: React.RefObject; +}) { + return ( + + ); +} + +function CardDescription({ + className, + ...props +}: TextProps & { + ref?: React.RefObject; +}) { + return ( + + ); +} + +function CardContent({ + className, + ...props +}: ViewProps & { + ref?: React.RefObject; +}) { + return ( + + + + ); +} + +function CardFooter({ + className, + ...props +}: ViewProps & { + ref?: React.RefObject; +}) { + return ( + + ); +} + +export { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +}; diff --git a/with-router-rnr/src/components/ui/progress.tsx b/with-router-rnr/src/components/ui/progress.tsx new file mode 100644 index 00000000..4567e4e4 --- /dev/null +++ b/with-router-rnr/src/components/ui/progress.tsx @@ -0,0 +1,84 @@ +import * as ProgressPrimitive from "@rn-primitives/progress"; +import * as React from "react"; +import { View } from "react-native"; +import Animated, { + Extrapolation, + interpolate, + useAnimatedStyle, + useDerivedValue, + withSpring, +} from "react-native-reanimated"; +import { cn } from "@/lib/utils"; + +function Progress({ + className, + value, + indicatorClassName, + ...props +}: ProgressPrimitive.RootProps & { + ref?: React.RefObject; + indicatorClassName?: string; +}) { + return ( + + + + ); +} + +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 (process.env.EXPO_OS === "web") { + return ( + + + + ); + } + + return ( + + + + ); +} diff --git a/with-router-rnr/src/components/ui/text.tsx b/with-router-rnr/src/components/ui/text.tsx new file mode 100644 index 00000000..5aad6e7f --- /dev/null +++ b/with-router-rnr/src/components/ui/text.tsx @@ -0,0 +1,30 @@ +import * as Slot from "@rn-primitives/slot"; +import * as React from "react"; +import { Text as RNText } from "react-native"; +import { cn } from "@/lib/utils"; + +const TextClassContext = React.createContext(undefined); + +function Text({ + className, + asChild = false, + ...props +}: React.ComponentProps & { + ref?: React.RefObject; + asChild?: boolean; +}) { + const textClass = React.use(TextClassContext); + const Component = asChild ? Slot.Text : RNText; + return ( + + ); +} + +export { Text, TextClassContext }; diff --git a/with-router-rnr/src/components/ui/tooltip.tsx b/with-router-rnr/src/components/ui/tooltip.tsx new file mode 100644 index 00000000..ebe25f53 --- /dev/null +++ b/with-router-rnr/src/components/ui/tooltip.tsx @@ -0,0 +1,48 @@ +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; + +function TooltipContent({ + className, + sideOffset = 4, + portalHost, + ...props +}: TooltipPrimitive.ContentProps & { + ref?: React.RefObject; + portalHost?: string; +}) { + return ( + + + + + + + + + + ); +} + +export { Tooltip, TooltipContent, TooltipTrigger }; diff --git a/with-router-rnr/src/global.css b/with-router-rnr/src/global.css new file mode 100644 index 00000000..525e2e9e --- /dev/null +++ b/with-router-rnr/src/global.css @@ -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%; + } +} \ No newline at end of file diff --git a/with-router-rnr/src/lib/android-navigation-bar.ts b/with-router-rnr/src/lib/android-navigation-bar.ts new file mode 100644 index 00000000..f0f70551 --- /dev/null +++ b/with-router-rnr/src/lib/android-navigation-bar.ts @@ -0,0 +1,10 @@ +import * as NavigationBar from "expo-navigation-bar"; +import { NAV_THEME } from "@/lib/constants"; + +export async function setAndroidNavigationBar(theme: "light" | "dark") { + if (process.env.EXPO_OS !== "android") return; + await NavigationBar.setButtonStyleAsync(theme === "dark" ? "light" : "dark"); + await NavigationBar.setBackgroundColorAsync( + theme === "dark" ? NAV_THEME.dark.background : NAV_THEME.light.background + ); +} diff --git a/with-router-rnr/src/lib/constants.ts b/with-router-rnr/src/lib/constants.ts new file mode 100644 index 00000000..c6822a37 --- /dev/null +++ b/with-router-rnr/src/lib/constants.ts @@ -0,0 +1,20 @@ +import * as AC from "@bacons/apple-colors"; + +export const NAV_THEME = { + light: { + background: AC.systemBackground, // background + border: AC.separator, // border + card: AC.secondarySystemBackground, // card + notification: AC.systemRed, // destructive + primary: AC.secondaryLabel, // primary + text: AC.label, // foreground + }, + dark: { + background: AC.systemBackground, // background + border: AC.separator, // border + card: AC.secondarySystemBackground, // card + notification: AC.systemRed, // destructive + primary: AC.secondaryLabel, // primary + text: AC.label, // foreground + }, +}; diff --git a/with-router-rnr/src/lib/icons/Info.tsx b/with-router-rnr/src/lib/icons/Info.tsx new file mode 100644 index 00000000..70282e4b --- /dev/null +++ b/with-router-rnr/src/lib/icons/Info.tsx @@ -0,0 +1,4 @@ +import { Info } from 'lucide-react-native'; +import { iconWithClassName } from './iconWithClassName'; +iconWithClassName(Info); +export { Info }; \ No newline at end of file diff --git a/with-router-rnr/src/lib/icons/MoonStar.tsx b/with-router-rnr/src/lib/icons/MoonStar.tsx new file mode 100644 index 00000000..c884cd1a --- /dev/null +++ b/with-router-rnr/src/lib/icons/MoonStar.tsx @@ -0,0 +1,4 @@ +import { MoonStar } from 'lucide-react-native'; +import { iconWithClassName } from './iconWithClassName'; +iconWithClassName(MoonStar); +export { MoonStar }; \ No newline at end of file diff --git a/with-router-rnr/src/lib/icons/Sun.tsx b/with-router-rnr/src/lib/icons/Sun.tsx new file mode 100644 index 00000000..3e2a642d --- /dev/null +++ b/with-router-rnr/src/lib/icons/Sun.tsx @@ -0,0 +1,4 @@ +import { Sun } from 'lucide-react-native'; +import { iconWithClassName } from './iconWithClassName'; +iconWithClassName(Sun); +export { Sun }; \ No newline at end of file diff --git a/with-router-rnr/src/lib/icons/iconWithClassName.ts b/with-router-rnr/src/lib/icons/iconWithClassName.ts new file mode 100644 index 00000000..c475ed11 --- /dev/null +++ b/with-router-rnr/src/lib/icons/iconWithClassName.ts @@ -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, + }, + }, + }); +} diff --git a/with-router-rnr/src/lib/useColorScheme.tsx b/with-router-rnr/src/lib/useColorScheme.tsx new file mode 100644 index 00000000..8e171b6e --- /dev/null +++ b/with-router-rnr/src/lib/useColorScheme.tsx @@ -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, + }; +} diff --git a/with-router-rnr/src/lib/useGlobals.android.ts b/with-router-rnr/src/lib/useGlobals.android.ts new file mode 100644 index 00000000..20a065f5 --- /dev/null +++ b/with-router-rnr/src/lib/useGlobals.android.ts @@ -0,0 +1,10 @@ +import { setAndroidNavigationBar } from "./android-navigation-bar"; +import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; +import { Appearance } from "react-native"; + +// A hook that runs in the root layout to set global styles and behaviors. +export function useGlobals() { + useIsomorphicLayoutEffect(() => { + setAndroidNavigationBar(Appearance.getColorScheme() ?? "light"); + }, []); +} diff --git a/with-router-rnr/src/lib/useGlobals.ts b/with-router-rnr/src/lib/useGlobals.ts new file mode 100644 index 00000000..e990f423 --- /dev/null +++ b/with-router-rnr/src/lib/useGlobals.ts @@ -0,0 +1,2 @@ +// A hook that runs in the root layout to set global styles and behaviors. +export function useGlobals() {} diff --git a/with-router-rnr/src/lib/useGlobals.web.ts b/with-router-rnr/src/lib/useGlobals.web.ts new file mode 100644 index 00000000..c759e29f --- /dev/null +++ b/with-router-rnr/src/lib/useGlobals.web.ts @@ -0,0 +1,9 @@ +import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; + +// A hook that runs in the root layout to set global styles and behaviors. +export function useGlobals() { + useIsomorphicLayoutEffect(() => { + // Adds the background color to the html element to prevent white background on overscroll. + document.documentElement.classList.add("bg-background"); + }, []); +} diff --git a/with-router-rnr/src/lib/useIsomorphicLayoutEffect.ts b/with-router-rnr/src/lib/useIsomorphicLayoutEffect.ts new file mode 100644 index 00000000..19de1174 --- /dev/null +++ b/with-router-rnr/src/lib/useIsomorphicLayoutEffect.ts @@ -0,0 +1,4 @@ +import * as React from "react"; + +export const useIsomorphicLayoutEffect = + typeof window === "undefined" ? React.useEffect : React.useLayoutEffect; diff --git a/with-router-rnr/src/lib/utils.ts b/with-router-rnr/src/lib/utils.ts new file mode 100644 index 00000000..2819a830 --- /dev/null +++ b/with-router-rnr/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/with-router-rnr/tailwind.config.js b/with-router-rnr/tailwind.config.js new file mode 100644 index 00000000..4e08c947 --- /dev/null +++ b/with-router-rnr/tailwind.config.js @@ -0,0 +1,68 @@ +const { hairlineWidth } = require("nativewind/theme"); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: "class", + content: ["./src/**/*.{js,jsx,ts,tsx}", "./node_modules/@rnr/**/*.{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", + }, + }, + }, + future: { + hoverOnlyWhenSupported: true, + }, + plugins: [require("tailwindcss-animate")], +}; diff --git a/with-router-rnr/tsconfig.json b/with-router-rnr/tsconfig.json new file mode 100644 index 00000000..decdf0b6 --- /dev/null +++ b/with-router-rnr/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "extends": "expo/tsconfig.base", + "include": [ + "global.d.ts", + "**/*.ts", + "**/*.tsx", + "nativewind-env.d.ts" + ] +} \ No newline at end of file