add unistyles

This commit is contained in:
Aman Varshney
2025-05-07 14:29:11 +05:30
parent d09a284ce7
commit 6c269a4c5b
74 changed files with 1762 additions and 208 deletions

View File

@@ -0,0 +1,25 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# expo router
expo-env.d.ts
.env
.cache
ios
android
# macOS
.DS_Store
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*

View File

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

View File

@@ -0,0 +1,46 @@
{
"expo": {
"name": "my-better-t-app",
"slug": "my-better-t-app",
"version": "1.0.0",
"scheme": "my-better-t-app",
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/favicon.png"
},
"plugins": [
"expo-router",
"expo-secure-store",
"expo-web-browser"
],
"experiments": {
"typedRoutes": true,
"tsconfigPaths": true
},
"newArchEnabled": true,
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.amanvarshney01.mybettertapp"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.amanvarshney01.mybettertapp",
"edgeToEdgeEnabled": true
}
}
}

View File

@@ -0,0 +1,29 @@
import { Tabs } from "expo-router";
import { TabBarIcon } from "@/components/tabbar-icon";
export default function TabLayout() {
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarActiveTintColor: "black",
}}
>
<Tabs.Screen
name="index"
options={{
title: "Tab One",
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
}}
/>
<Tabs.Screen
name="two"
options={{
title: "Tab Two",
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
}}
/>
</Tabs>
);
}

View File

@@ -0,0 +1,17 @@
import { Container } from "@/components/container";
import { Text, View } from "react-native";
export default function TabOne() {
return (
<Container>
<View className="p-6 flex-1 justify-center">
<Text className="text-2xl font-bold text-foreground text-center mb-4">
Tab One
</Text>
<Text className="text-foreground text-center">
This is the first tab of the application.
</Text>
</View>
</Container>
);
}

View File

@@ -0,0 +1,17 @@
import { Container } from "@/components/container";
import { Text, View } from "react-native";
export default function TabTwo() {
return (
<Container>
<View className="p-6 flex-1 justify-center">
<Text className="text-2xl font-bold text-foreground text-center mb-4">
Tab Two
</Text>
<Text className="text-foreground text-center">
This is the second tab of the application.
</Text>
</View>
</Container>
);
}

View File

@@ -0,0 +1,39 @@
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
import { Link } from "expo-router";
import { Drawer } from "expo-router/drawer";
import { HeaderButton } from "@/components/header-button";
const DrawerLayout = () => {
return (
<Drawer>
<Drawer.Screen
name="index"
options={{
headerTitle: "Home",
drawerLabel: "Home",
drawerIcon: ({ size, color }) => (
<Ionicons name="home-outline" size={size} color={color} />
),
}}
/>
<Drawer.Screen
name="(tabs)"
options={{
headerTitle: "Tabs",
drawerLabel: "Tabs",
drawerIcon: ({ size, color }) => (
<MaterialIcons name="border-bottom" size={size} color={color} />
),
headerRight: () => (
<Link href="/modal" asChild>
<HeaderButton />
</Link>
),
}}
/>
</Drawer>
);
};
export default DrawerLayout;

View File

@@ -0,0 +1,74 @@
import { View, Text, ScrollView } from "react-native";
import { Container } from "@/components/container";
{{#if (eq api "orpc")}}
import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { useQuery } from "@tanstack/react-query";
import { trpc } from "@/utils/trpc";
{{/if}}
{{#if (eq backend "convex")}}
import { useQuery } from "convex/react";
import { api } from "@{{ projectName }}/backend/convex/_generated/api.js";
{{/if}}
export default function Home() {
{{#if (eq api "orpc")}}
const healthCheck = useQuery(orpc.healthCheck.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
{{/if}}
{{#if (eq backend "convex")}}
const healthCheck = useQuery(api.healthCheck.get);
{{/if}}
return (
<Container>
<ScrollView className="py-4 flex-1">
<Text className="font-mono text-foreground text-2xl font-bold mb-6">
BETTER T STACK
</Text>
<View className="rounded-lg border border-foreground p-4">
<Text className="mb-2 font-medium text-foreground">API Status</Text>
<View className="flex-row items-center gap-2">
<View
className={`h-2.5 w-2.5 rounded-full ${
{{#if (or (eq api "orpc") (eq api "trpc"))}}
healthCheck.data ? "bg-green-500" : "bg-red-500"
{{else}}
healthCheck ? "bg-green-500" : "bg-red-500"
{{/if}}
}`}
/>
<Text className="text-sm text-foreground">
{{#if (eq api "orpc")}}
{healthCheck.isLoading
? "Checking..."
: healthCheck.data
? "Connected"
: "Disconnected"}
{{/if}}
{{#if (eq api "trpc")}}
{healthCheck.isLoading
? "Checking..."
: healthCheck.data
? "Connected"
: "Disconnected"}
{{/if}}
{{#if (eq backend "convex")}}
{healthCheck === undefined
? "Checking..."
: healthCheck === "OK"
? "Connected"
: "Error"}
{{/if}}
</Text>
</View>
</View>
</ScrollView>
</Container>
);
}

View File

@@ -0,0 +1,47 @@
import { ScrollViewStyleReset } from 'expo-router/html';
import { ReactNode } from 'react';
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta content="IE=edge" httpEquiv="X-UA-Compatible" />
{/*
This viewport disables scaling which makes the mobile website act more like a native app.
However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
*/}
<meta
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1.00001,viewport-fit=cover"
name="viewport"
/>
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`;

View File

@@ -0,0 +1,18 @@
import { Link, Stack } from 'expo-router';
import { Text } from 'react-native';
import { Container } from '@/components/container';
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<Container>
<Text className="text-xl font-bold">This screen doesn't exist.</Text>
<Link href="/" className="mt-4 pt-4">
<Text className="text-base text-[#2e78b7]">Go to home screen!</Text>
</Link>
</Container>
</>
);
}

View File

@@ -0,0 +1,106 @@
{{#if (eq backend "convex")}}
import { ConvexProvider, ConvexReactClient } from "convex/react";
{{else}}
import { QueryClientProvider } from "@tanstack/react-query";
{{/if}}
import { Stack } from "expo-router";
import {
DarkTheme,
DefaultTheme,
type Theme,
ThemeProvider,
} from "@react-navigation/native";
import { StatusBar } from "expo-status-bar";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import "../global.css";
{{#if (eq api "trpc")}}
import { queryClient } from "@/utils/trpc";
{{/if}}
{{#if (eq api "orpc")}}
import { queryClient } from "@/utils/orpc";
{{/if}}
import { NAV_THEME } from "@/lib/constants";
import React, { useRef } from "react";
import { useColorScheme } from "@/lib/use-color-scheme";
import { Platform } from "react-native";
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 const unstable_settings = {
initialRouteName: "(drawer)",
};
{{#if (eq backend "convex")}}
const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!, {
unsavedChangesWarning: false,
});
{{/if}}
export default function RootLayout() {
const hasMounted = useRef(false);
const { colorScheme, isDarkColorScheme } = useColorScheme();
const [isColorSchemeLoaded, setIsColorSchemeLoaded] = React.useState(false);
useIsomorphicLayoutEffect(() => {
if (hasMounted.current) {
return;
}
if (Platform.OS === "web") {
document.documentElement.classList.add("bg-background");
}
setAndroidNavigationBar(colorScheme);
setIsColorSchemeLoaded(true);
hasMounted.current = true;
}, []);
if (!isColorSchemeLoaded) {
return null;
}
return (
{{#if (eq backend "convex")}}
<ConvexProvider client={convex}>
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
<GestureHandlerRootView style=\{{ flex: 1 }}>
<Stack>
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
<Stack.Screen
name="modal"
options=\{{ title: "Modal", presentation: "modal" }}
/>
</Stack>
</GestureHandlerRootView>
</ThemeProvider>
</ConvexProvider>
{{else}}
<QueryClientProvider client={queryClient}>
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
<GestureHandlerRootView style=\{{ flex: 1 }}>
<Stack>
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
<Stack.Screen
name="modal"
options=\{{ title: "Modal", presentation: "modal" }}
/>
</Stack>
</GestureHandlerRootView>
</ThemeProvider>
</QueryClientProvider>
{{/if}}
);
}
const useIsomorphicLayoutEffect =
Platform.OS === "web" && typeof window === "undefined"
? React.useEffect
: React.useLayoutEffect;

View File

@@ -0,0 +1,12 @@
import { Container } from "@/components/container";
import { Text, View } from "react-native";
export default function Modal() {
return (
<Container>
<View className="flex-1 justify-center items-center">
<Text className="text-xl font-bold text-foreground">Modal View</Text>
</View>
</Container>
);
}

View File

@@ -0,0 +1,11 @@
module.exports = function (api) {
api.cache(true);
const plugins = [];
plugins.push('react-native-reanimated/plugin');
return {
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
plugins,
};
};

View File

@@ -0,0 +1,9 @@
import { SafeAreaView } from "react-native";
export const Container = ({ children }: { children: React.ReactNode }) => {
return (
<SafeAreaView className="flex flex-1 p-4 bg-background">
{children}
</SafeAreaView>
);
};

View File

@@ -0,0 +1,31 @@
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { forwardRef } from 'react';
import { Pressable, StyleSheet } from 'react-native';
export const HeaderButton = forwardRef<typeof Pressable, { onPress?: () => void }>(
({ onPress }, ref) => {
return (
<Pressable onPress={onPress}>
{({ pressed }) => (
<FontAwesome
name="info-circle"
size={25}
color="gray"
style={[
styles.headerRight,
{
opacity: pressed ? 0.5 : 1,
},
]}
/>
)}
</Pressable>
);
}
);
export const styles = StyleSheet.create({
headerRight: {
marginRight: 15,
},
});

View File

@@ -0,0 +1,15 @@
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { StyleSheet } from 'react-native';
export const TabBarIcon = (props: {
name: React.ComponentProps<typeof FontAwesome>['name'];
color: string;
}) => {
return <FontAwesome size={28} style={styles.tabBarIcon} {...props} />;
};
export const styles = StyleSheet.create({
tabBarIcon: {
marginBottom: -3,
},
});

View File

@@ -0,0 +1,25 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--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%;
--destructive: 0 84.2% 60.2%;
}
.dark:root {
--background: 240 10% 3.9%;
--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%;
--destructive: 0 72% 51%;
}
}

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,
);
}

View File

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

View File

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

View File

@@ -0,0 +1,59 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");
const { FileStore } = require("metro-cache");
const { withNativeWind } = require("nativewind/metro");
const path = require("node:path");
const config = withTurborepoManagedCache(
withMonorepoPaths(
withNativeWind(getDefaultConfig(__dirname), {
input: "./global.css",
configPath: "./tailwind.config.js",
}),
),
);
config.resolver.unstable_enablePackageExports = true;
config.resolver.disableHierarchicalLookup = true;
module.exports = config;
/**
* Add the monorepo paths to the Metro config.
* This allows Metro to resolve modules from the monorepo.
*
* @see https://docs.expo.dev/guides/monorepos/#modify-the-metro-config
* @param {import('expo/metro-config').MetroConfig} config
* @returns {import('expo/metro-config').MetroConfig}
*/
function withMonorepoPaths(config) {
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "../..");
// #1 - Watch all files in the monorepo
config.watchFolders = [workspaceRoot];
// #2 - Resolve modules within the project's `node_modules` first, then all monorepo modules
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(workspaceRoot, "node_modules"),
];
return config;
}
/**
* Move the Metro cache to the `.cache/metro` folder.
* If you have any environment variables, you can configure Turborepo to invalidate it when needed.
*
* @see https://turbo.build/repo/docs/reference/configuration#env
* @param {import('expo/metro-config').MetroConfig} config
* @returns {import('expo/metro-config').MetroConfig}
*/
function withTurborepoManagedCache(config) {
config.cacheStores = [
new FileStore({ root: path.join(__dirname, ".cache/metro") }),
];
return config;
}

View File

@@ -0,0 +1,45 @@
{
"name": "native",
"version": "1.0.0",
"main": "expo-router/entry",
"scripts": {
"dev": "expo start --clear",
"android": "expo run:android",
"ios": "expo run:ios",
"prebuild": "expo prebuild",
"web": "expo start --web"
},
"dependencies": {
"@expo/vector-icons": "^14.0.4",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/drawer": "^7.1.1",
"@react-navigation/native": "^7.0.14",
"@tanstack/react-form": "^1.0.5",
"@tanstack/react-query": "^5.69.2",
"expo": "^53.0.4",
"expo-constants": "~17.1.4",
"expo-linking": "~7.1.4",
"expo-navigation-bar": "~4.2.3",
"expo-router": "~5.0.3",
"expo-secure-store": "~14.2.3",
"expo-status-bar": "~2.2.3",
"expo-system-ui": "~5.0.6",
"expo-web-browser": "~14.1.6",
"nativewind": "^4.1.23",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.1",
"react-native-gesture-handler": "~2.24.0",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.3.0",
"react-native-screens": "~4.10.0",
"react-native-web": "^0.20.0"
},
"devDependencies": {
"@babel/core": "^7.26.10",
"@types/react": "~19.0.10",
"tailwindcss": "^3.4.17",
"typescript": "~5.8.2"
},
"private": true
}

View File

@@ -0,0 +1,32 @@
const { hairlineWidth } = require("nativewind/theme");
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: ["./app/**/*.{js,ts,tsx}", "./components/**/*.{js,ts,tsx}"],
presets: [require("nativewind/preset")],
theme: {
extend: {
colors: {
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))",
},
},
borderWidth: {
hairline: hairlineWidth(),
},
},
},
plugins: [],
};

View File

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