diff --git a/api/index.ts b/api/index.ts index 0d08af4..11d2330 100644 --- a/api/index.ts +++ b/api/index.ts @@ -1,6 +1,6 @@ -const { createRequestHandler } = require("expo-server/adapter/vercel"); -const path = require("node:path"); +const { createRequestHandler } = require('expo-server/adapter/vercel'); +const path = require('node:path'); module.exports = createRequestHandler({ - build: path.join(__dirname, "../dist/server"), + build: path.join(__dirname, '../dist/server'), }); diff --git a/app.config.ts b/app.config.ts new file mode 100644 index 0000000..979bc31 --- /dev/null +++ b/app.config.ts @@ -0,0 +1,78 @@ +import type { ConfigContext, ExpoConfig } from 'expo/config'; +import packageJson from './package.json'; + +export const config: ExpoConfig = { + name: 'Native Template', + description: packageJson.description, + slug: packageJson.name, + version: packageJson.version, + orientation: 'portrait', + icon: './assets/logo.png', + scheme: packageJson.name, + userInterfaceStyle: 'automatic', + newArchEnabled: true, + ios: { + supportsTablet: true, + bundleIdentifier: 'com.sohanemon.nativetemplate', + infoPlist: { + ITSAppUsesNonExemptEncryption: false, + }, + icon: { + light: './assets/icons/ios-light.png', + dark: './assets/icons/ios-dark.png', + tinted: './assets/icons/ios-tinted.png', + }, + }, + android: { + adaptiveIcon: { + backgroundColor: '#E6F4FE', + foregroundImage: './assets/icons/adaptive-icon.png', + monochromeImage: './assets/icons/adaptive-icon.png', + }, + edgeToEdgeEnabled: true, + package: 'com.sohanemon.nativetemplate', + }, + web: { + output: 'server', + bundler: 'metro', + favicon: './assets/favicon.png', + }, + plugins: [ + [ + 'expo-router', + { + unstable_useServerMiddleware: true, + }, + ], + [ + 'expo-splash-screen', + { + image: './assets/icons/splash-icon-light.png', + imageWidth: 200, + resizeMode: 'contain', + backgroundColor: '#ffffff', + dark: { + image: './assets/icons/splash-icon-dark.png', + backgroundColor: '#000000', + }, + }, + ], + ], + experiments: { + typedRoutes: true, + reactCompiler: true, + }, + extra: { + router: { + unstable_useServerMiddleware: true, + }, + eas: { + projectId: '003e3a84-f6d1-4c5f-9451-5be32b0c90f8', + }, + }, +}; + +export default ({ config: defaultConfig }: ConfigContext): ExpoConfig => ({ + ...defaultConfig, + ...config, +}); diff --git a/app.json b/app.json deleted file mode 100644 index 8db25a5..0000000 --- a/app.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "expo": { - "name": "native-template", - "slug": "native-template", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/icon.png", - "scheme": "nativetemplate", - "userInterfaceStyle": "automatic", - "newArchEnabled": true, - "ios": { - "supportsTablet": true, - "bundleIdentifier": "com.sohanemon.nativetemplate", - "infoPlist": { - "ITSAppUsesNonExemptEncryption": false - } - }, - "android": { - "adaptiveIcon": { - "backgroundColor": "#E6F4FE", - "foregroundImage": "./assets/adaptive-icon.png" - }, - "edgeToEdgeEnabled": true, - "package": "com.anonymous.nativetemplate" - }, - "web": { - "output": "server", - "bundler": "metro", - "favicon": "./assets/favicon.png" - }, - "plugins": [ - [ - "expo-router", - { - "unstable_useServerMiddleware": true - } - ], - [ - "expo-splash-screen", - { - "image": "./assets/splash.png", - "imageWidth": 200, - "resizeMode": "contain", - "backgroundColor": "#ffffff", - "dark": { - "backgroundColor": "#000000" - } - } - ] - ], - "experiments": { - "typedRoutes": true, - "reactCompiler": true - }, - "extra": { - "router": { - "unstable_useServerMiddleware": true - }, - "eas": { - "projectId": "003e3a84-f6d1-4c5f-9451-5be32b0c90f8" - } - } - } -} diff --git a/app/(drawer)/(tabs)/_layout.tsx b/app/(drawer)/(tabs)/_layout.tsx deleted file mode 100644 index 8f2b0b7..0000000 --- a/app/(drawer)/(tabs)/_layout.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Tabs } from "expo-router"; -import { useThemeColor } from "heroui-native"; - -export default function TabLayout() { - const themeColorForeground = useThemeColor("foreground"); - const themeColorBackground = useThemeColor("background"); - - return ( - - ( - - ), - }} - /> - ( - - ), - }} - /> - - ); -} diff --git a/app/(drawer)/(tabs)/index.tsx b/app/(drawer)/(tabs)/index.tsx deleted file mode 100644 index 774c1c3..0000000 --- a/app/(drawer)/(tabs)/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Card } from "heroui-native"; -import { View } from "react-native"; - -import { Container } from "@/components/container"; - -export default function Home() { - return ( - - - - Tab One - - - - ); -} diff --git a/app/(drawer)/(tabs)/two.tsx b/app/(drawer)/(tabs)/two.tsx deleted file mode 100644 index 4ec5ddb..0000000 --- a/app/(drawer)/(tabs)/two.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Card } from "heroui-native"; -import { View } from "react-native"; - -import { Container } from "@/components/container"; - -export default function TabTwo() { - return ( - - - - TabTwo - - - - ); -} diff --git a/app/(drawer)/_layout.tsx b/app/(drawer)/_layout.tsx deleted file mode 100644 index 863236b..0000000 --- a/app/(drawer)/_layout.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Ionicons, MaterialIcons } from "@expo/vector-icons"; -import { Drawer } from "expo-router/drawer"; -import { useThemeColor } from "heroui-native"; -import { useCallback } from "react"; - -import { ThemeToggle } from "@/components/theme-toggle"; -import { Typography } from "@/components/ui/typography"; - -function DrawerLayout() { - const themeColorForeground = useThemeColor("foreground"); - const themeColorBackground = useThemeColor("background"); - - const renderThemeToggle = useCallback(() => , []); - - return ( - - ( - - Home - - ), - drawerIcon: ({ size, color, focused }) => ( - - ), - }} - /> - ( - - Tabs - - ), - drawerIcon: ({ size, color, focused }) => ( - - ), - }} - /> - - ); -} - -export default DrawerLayout; diff --git a/app/(drawer)/index.tsx b/app/(drawer)/index.tsx deleted file mode 100644 index babfb84..0000000 --- a/app/(drawer)/index.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import { Button, Card, Chip, useThemeColor, useToast } from "heroui-native"; -import { View } from "react-native"; - -import { Container } from "@/components/container"; -import { Typography } from "@/components/ui/typography"; -import { api } from "@/lib/trpc/api"; - -export default function Home() { - const { isLoading, data, refetch, isRefetching } = - api.healthcheck.check.useQuery(); - const mutedColor = useThemeColor("muted"); - const successColor = useThemeColor("success"); - const dangerColor = useThemeColor("danger"); - const { toast } = useToast(); - - const isConnected = data?.status === "OK"; - - return ( - - - - Native Template - - - - - - System Status - - {isConnected ? "LIVE" : "OFFLINE"} - - - - - - - - TRPC Backend - - - {isLoading - ? "Checking connection..." - : isConnected - ? "Connected to API" - : "API Disconnected"} - - - {isLoading && ( - - )} - {!isLoading && isConnected && ( - - )} - {!isLoading && !isConnected && ( - - )} - - - - - - ); -} diff --git a/app/(stack)/(drawer)/(tabs)/_layout.tsx b/app/(stack)/(drawer)/(tabs)/_layout.tsx new file mode 100644 index 0000000..e64c481 --- /dev/null +++ b/app/(stack)/(drawer)/(tabs)/_layout.tsx @@ -0,0 +1,48 @@ +import { Tabs } from 'expo-router/tabs'; +import { Icon } from '@/components/icon'; +import { useTheme } from '@/lib/context/theme'; + +export default function TabLayout() { + const { colors } = useTheme(); + + return ( + + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); +} diff --git a/app/(stack)/(drawer)/(tabs)/index.tsx b/app/(stack)/(drawer)/(tabs)/index.tsx new file mode 100644 index 0000000..e476def --- /dev/null +++ b/app/(stack)/(drawer)/(tabs)/index.tsx @@ -0,0 +1,31 @@ +import { useRouter } from 'expo-router'; +import { View } from 'react-native'; +import { GestureDetector } from 'react-native-gesture-handler'; +import { Container } from '@/components/layout/container'; +import { Card, CardDescription, CardTitle } from '@/components/ui/card'; +import { swipeGesture } from '@/lib/utils/swipe-gesture-handler'; + +export default function Home() { + const router = useRouter(); + + return ( + { + router.push('./two'); + }, + })} + > + + + + + Tab 1 + Swipe Left to Right for tab-2 + + + + + + ); +} diff --git a/app/(stack)/(drawer)/(tabs)/two.tsx b/app/(stack)/(drawer)/(tabs)/two.tsx new file mode 100644 index 0000000..0844e30 --- /dev/null +++ b/app/(stack)/(drawer)/(tabs)/two.tsx @@ -0,0 +1,41 @@ +import { useRouter } from 'expo-router'; +import { View } from 'react-native'; +import { GestureDetector } from 'react-native-gesture-handler'; +import { Container } from '@/components/layout/container'; +import { Button } from '@/components/ui/button'; +import { Card, CardDescription, CardTitle } from '@/components/ui/card'; +import { swipeGesture } from '@/lib/utils/swipe-gesture-handler'; +import { Alert } from 'react-native'; + +export default function TabTwo() { + const router = useRouter(); + + return ( + { + router.push('./'); + }, + onUp: () => { + Alert.alert('☝ ', 'You just swiped up'); + }, + })} + > + + + + + Tab 2 + + Swipe Right to Left for tab-1.{'\n'} Swipe Up for Alert. + + + + + + + + + + ); +} diff --git a/app/(stack)/(drawer)/_layout.tsx b/app/(stack)/(drawer)/_layout.tsx new file mode 100644 index 0000000..2cfc6a8 --- /dev/null +++ b/app/(stack)/(drawer)/_layout.tsx @@ -0,0 +1,65 @@ +import { Drawer } from 'expo-router/drawer'; +import { Icon } from '@/components/icon'; +import { DrawerContents } from '@/components/layout/drawer/drawer-contents'; +import { DrawerToggle } from '@/components/layout/drawer/drawer-toggle'; +import { ThemeSelect } from '@/components/theme-select'; +import { Text } from '@/components/ui/text'; +import { useTheme } from '@/lib/context/theme'; + +export default function DrawerLayout() { + const { colors } = useTheme(); + + return ( + , + headerLeft: () => , + drawerStyle: { backgroundColor: colors.background }, + drawerActiveTintColor: colors.primary, + drawerInactiveTintColor: colors.muted, + headerShadowVisible: false, + swipeEnabled: true, + }} + drawerContent={DrawerContents} + > + ( + + Home + + ), + drawerIcon: ({ size, color, focused }) => ( + + ), + }} + /> + ( + + Tabs + + ), + drawerIcon: ({ size, color, focused }) => ( + + ), + }} + /> + + ); +} diff --git a/app/(stack)/(drawer)/index.tsx b/app/(stack)/(drawer)/index.tsx new file mode 100644 index 0000000..c10af7f --- /dev/null +++ b/app/(stack)/(drawer)/index.tsx @@ -0,0 +1,55 @@ +import { View } from 'react-native'; +import { Icon } from '@/components/icon'; +import { Container } from '@/components/layout/container'; +import { Card, CardDescription, CardTitle } from '@/components/ui/card'; +import { Text } from '@/components/ui/text'; +import { api } from '@/lib/trpc/api'; + +export default function Home() { + const { isLoading, data } = api.healthcheck.check.useQuery(); + + const isConnected = data?.status === 'OK'; + + return ( + + + + Native Template + + + + + + System Status + {isConnected ? 'LIVE' : 'OFFLINE'} + + + + + + TRPC Backend + + + {isLoading + ? 'Checking connection...' + : isConnected + ? 'Connected to API' + : 'API Disconnected'} + + + {isLoading && ( + + )} + {!isLoading && isConnected && ( + + )} + {!isLoading && !isConnected && ( + + )} + + + + ); +} diff --git a/app/(stack)/+not-found.tsx b/app/(stack)/+not-found.tsx new file mode 100644 index 0000000..2d03702 --- /dev/null +++ b/app/(stack)/+not-found.tsx @@ -0,0 +1,33 @@ +import { Link, Stack } from 'expo-router'; +import { Pressable, View } from 'react-native'; + +import { Container } from '@/components/layout/container'; +import { Card, CardDescription, CardTitle } from '@/components/ui/card'; +import { Text } from '@/components/ui/text'; +import { Button } from '@/components/ui/button'; + +export default function NotFoundScreen() { + return ( + <> + + + + + + 🤔 + + + Page Not Found + + + Sorry, the page you're looking for doesn't exist. + + + + + + + ); +} diff --git a/app/(stack)/_layout.tsx b/app/(stack)/_layout.tsx new file mode 100644 index 0000000..88c5b8c --- /dev/null +++ b/app/(stack)/_layout.tsx @@ -0,0 +1,27 @@ +import { Stack } from 'expo-router/stack'; +import { StackBack } from '@/components/layout/stack/stack-back'; +import { useTheme } from '@/lib/context/theme'; + +export const unstable_settings = { + initialRouteName: '(drawer)', +}; + +export default function StackLayout() { + const { colors } = useTheme(); + + return ( + , + headerShadowVisible: false, + }} + > + + + + ); +} diff --git a/app/(stack)/other.tsx b/app/(stack)/other.tsx new file mode 100644 index 0000000..6ff6d00 --- /dev/null +++ b/app/(stack)/other.tsx @@ -0,0 +1,19 @@ +import { View } from 'react-native'; +import { Container } from '@/components/layout/container'; +import { Icon } from '@/components/icon'; +import { Img } from '@/components/ui/image'; +import { Text } from '@/components/ui/text'; + +export default function DrawerOther() { + return ( + + + + + + A Sample image component + + + + ); +} diff --git a/app/+not-found.tsx b/app/+not-found.tsx deleted file mode 100644 index cf845fa..0000000 --- a/app/+not-found.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Link, Stack } from "expo-router"; -import { Card } from "heroui-native"; -import { Pressable, View } from "react-native"; - -import { Container } from "@/components/container"; -import { Typography } from "@/components/ui/typography"; - -export default function NotFoundScreen() { - return ( - <> - - - - - - 🤔 - - - Page Not Found - - - Sorry, the page you're looking for doesn't exist. - - - - - Go to Home - - - - - - - - ); -} diff --git a/app/_layout.tsx b/app/_layout.tsx index 59b7c12..8c08443 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,23 +1,21 @@ -import "@/styles/global.css"; -import { Stack } from "expo-router"; -import { Providers } from "@/lib/context/providers"; +import '@/styles/global.css'; +import { PortalHost } from '@rn-primitives/portal'; +import { Slot } from 'expo-router'; +import * as SplashScreen from 'expo-splash-screen'; +import { Providers } from '@/lib/context/providers'; export const unstable_settings = { - initialRouteName: "(drawer)", + initialRouteName: '(drawer)', }; -function StackLayout() { - return ( - - - - ); -} +SplashScreen.preventAutoHideAsync(); +SplashScreen.setOptions({ duration: 500, fade: true }); export default function Layout() { return ( - + + ); } diff --git a/app/trpc/[trpc]+api.ts b/app/trpc/[trpc]+api.ts index 8a561a2..ce16421 100644 --- a/app/trpc/[trpc]+api.ts +++ b/app/trpc/[trpc]+api.ts @@ -1,6 +1,6 @@ -import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; -import { appRouter } from "@/server/root"; -import { createTRPCContext } from "@/server/trpc"; +import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; +import { appRouter } from '@/server/root'; +import { createTRPCContext } from '@/server/trpc'; const createContext = async (req: Request) => { return createTRPCContext({ @@ -10,15 +10,15 @@ const createContext = async (req: Request) => { const handler = (req: Request) => fetchRequestHandler({ - endpoint: "/trpc", + endpoint: '/trpc', req, router: appRouter, createContext: () => createContext(req), onError: - process.env.NODE_ENV === "development" + process.env.NODE_ENV === 'development' ? ({ path, error }) => { console.error( - `❌ tRPC failed on ${path ?? ""}: ${error.message}`, + `❌ tRPC failed on ${path ?? ''}: ${error.message}`, ); } : undefined, diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png deleted file mode 100644 index 03d6f6b..0000000 Binary files a/assets/adaptive-icon.png and /dev/null differ diff --git a/assets/icon.png b/assets/icon.png deleted file mode 100644 index a0b1526..0000000 Binary files a/assets/icon.png and /dev/null differ diff --git a/assets/icons/adaptive-icon.png b/assets/icons/adaptive-icon.png new file mode 100644 index 0000000..69bc1b9 Binary files /dev/null and b/assets/icons/adaptive-icon.png differ diff --git a/assets/icons/ios-dark.png b/assets/icons/ios-dark.png new file mode 100644 index 0000000..843385e Binary files /dev/null and b/assets/icons/ios-dark.png differ diff --git a/assets/icons/ios-light.png b/assets/icons/ios-light.png new file mode 100644 index 0000000..9677c95 Binary files /dev/null and b/assets/icons/ios-light.png differ diff --git a/assets/icons/ios-tinted.png b/assets/icons/ios-tinted.png new file mode 100644 index 0000000..4450607 Binary files /dev/null and b/assets/icons/ios-tinted.png differ diff --git a/assets/icons/splash-icon-dark.png b/assets/icons/splash-icon-dark.png new file mode 100644 index 0000000..1bf4b76 Binary files /dev/null and b/assets/icons/splash-icon-dark.png differ diff --git a/assets/icons/splash-icon-light.png b/assets/icons/splash-icon-light.png new file mode 100644 index 0000000..31e40ca Binary files /dev/null and b/assets/icons/splash-icon-light.png differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..2120bb3 Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/splash.png b/assets/splash.png deleted file mode 100644 index 0e89705..0000000 Binary files a/assets/splash.png and /dev/null differ diff --git a/biome.json b/biome.json index 1d782d3..2dd0e68 100644 --- a/biome.json +++ b/biome.json @@ -1,10 +1,5 @@ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": false - }, "files": { "ignoreUnknown": false, "includes": [ @@ -15,13 +10,11 @@ "!**/.zed", "!**/.vscode", "!**/routeTree.gen.ts", - "!**/src-tauri", "!**/.nuxt", "!bts.jsonc", "!**/.expo", "!**/.wrangler", "!**/.alchemy", - "!**/.svelte-kit", "!**/wrangler.jsonc", "!**/.source", "!**/convex/_generated" @@ -37,13 +30,20 @@ "rules": { "recommended": true, "correctness": { - "useExhaustiveDependencies": "info" + "useExhaustiveDependencies": "info", + "noUnknownFunction": "info" }, "nursery": { "useSortedClasses": { "level": "warn", "fix": "safe", "options": { + "attributes": [ + "className", + "classList", + "wrapperClassName", + "wrapperClass" + ], "functions": ["clsx", "cva", "cn"] } } @@ -67,12 +67,23 @@ }, "javascript": { "formatter": { - "quoteStyle": "double" + "quoteStyle": "single", + "jsxQuoteStyle": "double" } }, "css": { "parser": { "tailwindDirectives": true } + }, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "json": { + "parser": { + "allowComments": true + } } } diff --git a/bun.lock b/bun.lock index e4aaa93..72f9958 100644 --- a/bun.lock +++ b/bun.lock @@ -10,24 +10,29 @@ "@gorhom/bottom-sheet": "^5.2.8", "@react-navigation/drawer": "^7.5.0", "@react-navigation/elements": "^2.9.3", + "@react-navigation/native": "^7.1.26", + "@rn-primitives/portal": "^1.3.0", + "@rn-primitives/select": "^1.2.0", + "@rn-primitives/slot": "^1.2.0", "@tanstack/react-query": "^5.90.12", "@trpc/client": "^11.8.1", "@trpc/react-query": "^11.8.1", "@trpc/server": "^11.8.1", "@trpc/tanstack-react-query": "^11.8.1", "@ts-utilities/core": "^1.0.6", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "expo": "^54.0.30", "expo-constants": "~18.0.12", "expo-font": "~14.0.10", "expo-haptics": "^15.0.8", + "expo-image": "^3.0.11", "expo-linking": "~8.0.11", "expo-network": "~8.0.8", "expo-router": "^6.0.21", "expo-secure-store": "~15.0.8", "expo-server": "^1.0.5", "expo-splash-screen": "^31.0.13", - "expo-status-bar": "~3.0.9", - "heroui-native": "1.0.0-beta.9", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.5", @@ -40,7 +45,6 @@ "react-native-web": "^0.21.2", "react-native-worklets": "0.5.1", "superjson": "^2.2.6", - "zod": "^4.2.1", }, "devDependencies": { "@biomejs/biome": "^2.3.10", @@ -49,11 +53,12 @@ "@types/react": "~19.1.10", "husky": "^9.1.7", "tailwind-merge": "^3.4.0", - "tailwind-variants": "3.2.2", "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.4.0", "typescript": "~5.9.2", "uniwind": "^1.2.2", "vitest": "^4.0.16", + "zod": "^4.2.1", }, }, }, @@ -372,6 +377,14 @@ "@expo/xcpretty": ["@expo/xcpretty@4.3.2", "", { "dependencies": { "@babel/code-frame": "7.10.4", "chalk": "^4.1.0", "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw=="], + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@gorhom/bottom-sheet": ["@gorhom/bottom-sheet@5.2.8", "", { "dependencies": { "@gorhom/portal": "1.0.14", "invariant": "^2.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-native": "*", "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.16.1", "react-native-reanimated": ">=3.16.0 || >=4.0.0-" }, "optionalPeers": ["@types/react", "@types/react-native"] }, "sha512-+N27SMpbBxXZQ/IA2nlEV6RGxL/qSFHKfdFKcygvW+HqPG5jVNb1OqehLQsGfBP+Up42i0gW5ppI+DhpB7UCzA=="], "@gorhom/portal": ["@gorhom/portal@1.0.14", "", { "dependencies": { "nanoid": "^3.3.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A=="], @@ -412,8 +425,12 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], @@ -432,6 +449,8 @@ "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], @@ -440,6 +459,8 @@ "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], @@ -454,6 +475,16 @@ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@react-native/assets-registry": ["@react-native/assets-registry@0.81.5", "", {}, "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w=="], "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.81.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.81.5" } }, "sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ=="], @@ -490,6 +521,16 @@ "@react-navigation/routers": ["@react-navigation/routers@7.5.3", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg=="], + "@rn-primitives/hooks": ["@rn-primitives/hooks@1.3.0", "", { "dependencies": { "@rn-primitives/types": "1.2.0" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-BR97reSu7uVDpyMeQdRJHT0w8KdS6jdYnOL6xQtqS2q3H6N7vXBlX4LFERqJZphD+aziJFIAJ3HJF1vtt6XlpQ=="], + + "@rn-primitives/portal": ["@rn-primitives/portal@1.3.0", "", { "dependencies": { "zustand": "^5.0.4" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-a2DSce7TcSfcs0cCngLadAJOvx/+mdH9NRu+GxkX8NPRsGGhJvDEOqouMgDqLwx7z9mjXoUaZcwaVcemUSW9/A=="], + + "@rn-primitives/select": ["@rn-primitives/select@1.2.0", "", { "dependencies": { "@radix-ui/react-select": "^2.2.5", "@rn-primitives/hooks": "1.3.0", "@rn-primitives/slot": "1.2.0", "@rn-primitives/types": "1.2.0" }, "peerDependencies": { "@rn-primitives/portal": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-W3qFkdSAFPnjNMM7II5MiLCItjWOGXr8f+3obPtLAHcWrcsX/d1KogmplWXwmhBvVStCgE1OpJAD3DE2CHx9Rw=="], + + "@rn-primitives/slot": ["@rn-primitives/slot@1.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-cpbn+JLjSeq3wcA4uqgFsUimMrWYWx2Ks7r5rkwd1ds1utxynsGkLOKpYVQkATwWrYhtcoF1raxIKEqXuMN+/w=="], + + "@rn-primitives/types": ["@rn-primitives/types@1.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-b+6zKgdKVqAfaFPSfhwlQL0dnPQXPpW890m3eguC0VDI1eOsoEvUfVb6lmgH4bum9MmI0xymq4tOUI/fsKLoCQ=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="], @@ -748,6 +789,8 @@ "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], @@ -758,6 +801,8 @@ "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -886,6 +931,8 @@ "expo-haptics": ["expo-haptics@15.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g=="], + "expo-image": ["expo-image@3.0.11", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-4TudfUCLgYgENv+f48omnU8tjS2S0Pd9EaON5/s1ZUBRwZ7K8acEr4NfvLPSaeXvxW24iLAiyQ7sV7BXQH3RoA=="], + "expo-keep-awake": ["expo-keep-awake@15.0.8", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ=="], "expo-linking": ["expo-linking@8.0.11", "", { "dependencies": { "expo-constants": "~18.0.12", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA=="], @@ -904,8 +951,6 @@ "expo-splash-screen": ["expo-splash-screen@31.0.13", "", { "dependencies": { "@expo/prebuild-config": "^54.0.8" }, "peerDependencies": { "expo": "*" } }, "sha512-1epJLC1cDlwwj089R2h8cxaU5uk4ONVAC+vzGiTZH4YARQhL4Stlz1MbR6yAS173GMosvkE6CAeihR7oIbCkDA=="], - "expo-status-bar": ["expo-status-bar@3.0.9", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-xyYyVg6V1/SSOZWh4Ni3U129XHCnFHBTcUo0dhWtFDrZbNp/duw5AGsQfb2sVeU0gxWHXSY1+5F0jnKYC7WuOw=="], - "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -966,8 +1011,6 @@ "hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="], - "heroui-native": ["heroui-native@1.0.0-beta.9", "", { "peerDependencies": { "@gorhom/bottom-sheet": "^5", "react": ">=19.0.0", "react-native": ">=0.81.0", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": ">=4", "react-native-svg": "15.12.1", "react-native-worklets": "0.5.1", "tailwind-merge": "^3.4.0", "tailwind-variants": "^3.2.2" } }, "sha512-3TMjTDl7JJw+q5XYaEPZ8MvErbdmUkb98B7+TKYfHhzPE4/VbpXq4lUpvzP6hXFMMe60TvGlt6Djn+DRwIjPew=="], - "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], @@ -1436,8 +1479,6 @@ "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], - "tailwind-variants": ["tailwind-variants@3.2.2", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="], - "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], @@ -1478,6 +1519,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], "type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], @@ -1578,6 +1621,8 @@ "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], + "zustand": ["zustand@5.0.9", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg=="], + "@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -1642,6 +1687,8 @@ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@react-native/community-cli-plugin/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@react-navigation/core/react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="], diff --git a/components.json b/components.json new file mode 100644 index 0000000..f55d60d --- /dev/null +++ b/components.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "styles/global.css", + "baseColor": "neutral", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/lib/hooks" + } +} diff --git a/components/icon.tsx b/components/icon.tsx index 76bca8e..f24e628 100644 --- a/components/icon.tsx +++ b/components/icon.tsx @@ -6,6 +6,7 @@ import { FontAwesome, FontAwesome5, FontAwesome6, + Fontisto, Foundation, Ionicons, MaterialCommunityIcons, @@ -13,23 +14,52 @@ import { Octicons, SimpleLineIcons, Zocial, -} from "@expo/vector-icons"; -import { withUniwind } from "uniwind"; +} from '@expo/vector-icons'; +import type * as React from 'react'; +import type { ComponentPropsWithoutRef, ElementType, FC } from 'react'; +import { withUniwind } from 'uniwind'; +import { type TextVariant, textVariants } from '@/components/ui/text'; +import { cn } from '@/lib/utils'; + +type VariantProps = { + variant?: TextVariant; + className?: string; +}; + +type IconProps = ComponentPropsWithoutRef & + VariantProps; + +const createIconWithVariants = ( + IconComponent: T, +): FC> => { + const UniwindIcon = withUniwind(IconComponent as React.ComponentType); + + const WrappedIcon: FC> = ({ className, variant, ...props }) => { + return ( + + ); + }; + + return WrappedIcon; +}; -// NOTE: icon namespace object export const Icon = { - Ionicons: withUniwind(Ionicons), - MaterialIcons: withUniwind(MaterialIcons), - MaterialCommunityIcons: withUniwind(MaterialCommunityIcons), - FontAwesome: withUniwind(FontAwesome), - FontAwesome5: withUniwind(FontAwesome5), - FontAwesome6: withUniwind(FontAwesome6), - Feather: withUniwind(Feather), - AntDesign: withUniwind(AntDesign), - Entypo: withUniwind(Entypo), - EvilIcons: withUniwind(EvilIcons), - Foundation: withUniwind(Foundation), - Octicons: withUniwind(Octicons), - SimpleLineIcons: withUniwind(SimpleLineIcons), - Zocial: withUniwind(Zocial), + Ionicons: createIconWithVariants(Ionicons), + MaterialIcons: createIconWithVariants(MaterialIcons), + MaterialCommunityIcons: createIconWithVariants(MaterialCommunityIcons), + FontAwesome: createIconWithVariants(FontAwesome), + FontAwesome5: createIconWithVariants(FontAwesome5), + FontAwesome6: createIconWithVariants(FontAwesome6), + Fontisto: createIconWithVariants(Fontisto), + Feather: createIconWithVariants(Feather), + AntDesign: createIconWithVariants(AntDesign), + Entypo: createIconWithVariants(Entypo), + EvilIcons: createIconWithVariants(EvilIcons), + Foundation: createIconWithVariants(Foundation), + Octicons: createIconWithVariants(Octicons), + SimpleLineIcons: createIconWithVariants(SimpleLineIcons), + Zocial: createIconWithVariants(Zocial), } as const; diff --git a/components/container.tsx b/components/layout/container.tsx similarity index 57% rename from components/container.tsx rename to components/layout/container.tsx index 8049bc5..e48c1e9 100644 --- a/components/container.tsx +++ b/components/layout/container.tsx @@ -1,8 +1,8 @@ -import { cn } from "heroui-native"; -import type { PropsWithChildren } from "react"; -import { ScrollView, View, type ViewProps } from "react-native"; -import Animated, { type AnimatedProps } from "react-native-reanimated"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; +import type { PropsWithChildren } from 'react'; +import { ScrollView, View, type ViewProps } from 'react-native'; +import Animated, { type AnimatedProps } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { cn } from '@/lib/utils'; const AnimatedView = Animated.createAnimatedComponent(View); @@ -19,7 +19,7 @@ export function Container({ return ( + + + + + + + + + + + + + ); +} diff --git a/components/layout/drawer/drawer-footer.tsx b/components/layout/drawer/drawer-footer.tsx new file mode 100644 index 0000000..c4a82d2 --- /dev/null +++ b/components/layout/drawer/drawer-footer.tsx @@ -0,0 +1,49 @@ +import { type ViewProps, Linking, TouchableOpacity, View } from 'react-native'; +import { Icon } from '@/components/icon'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; + +type DrawerFooterProps = ViewProps; + +export function DrawerFooter({ className, ...props }: DrawerFooterProps) { + return ( + + + + + + ); +} diff --git a/components/layout/drawer/drawer-header.tsx b/components/layout/drawer/drawer-header.tsx new file mode 100644 index 0000000..b9ce294 --- /dev/null +++ b/components/layout/drawer/drawer-header.tsx @@ -0,0 +1,29 @@ +import { View, type ViewProps } from 'react-native'; +import { Img } from '@/components/ui/image'; +import { Text } from '@/components/ui/text'; +import { appConfig } from '@/lib/config/app'; +import { cn } from '@/lib/utils'; + +type DrawerHeaderProps = ViewProps; + +export function DrawerHeader({ className, ...props }: DrawerHeaderProps) { + return ( + + + + + + {appConfig.name} + + v{appConfig.version} + + ); +} diff --git a/components/layout/drawer/drawer-toggle.tsx b/components/layout/drawer/drawer-toggle.tsx new file mode 100644 index 0000000..58761bd --- /dev/null +++ b/components/layout/drawer/drawer-toggle.tsx @@ -0,0 +1,17 @@ +import { DrawerActions } from '@react-navigation/native'; +import { useNavigation } from 'expo-router'; +import { Icon } from '@/components/icon'; + +export const DrawerToggle = () => { + const router = useNavigation(); + + return ( + { + router.dispatch(DrawerActions.openDrawer()); + }} + /> + ); +}; diff --git a/components/layout/stack/stack-back.tsx b/components/layout/stack/stack-back.tsx new file mode 100644 index 0000000..48d36b0 --- /dev/null +++ b/components/layout/stack/stack-back.tsx @@ -0,0 +1,22 @@ +import { useNavigation } from 'expo-router'; +import type { ViewProps } from 'react-native'; +import { Icon } from '@/components/icon'; +import { cn } from '@/lib/utils'; + +type StackBackProps = ViewProps; + +export function StackBack({ className, ...props }: StackBackProps) { + const router = useNavigation(); + + return ( + { + router.goBack(); + }} + {...props} + /> + ); +} diff --git a/components/theme-select.tsx b/components/theme-select.tsx new file mode 100644 index 0000000..5d0742d --- /dev/null +++ b/components/theme-select.tsx @@ -0,0 +1,97 @@ +import * as Haptics from 'expo-haptics'; +import { type ThemeName, useTheme } from '@/lib/context/theme'; +import { Icon } from './icon'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, +} from './ui/select'; + +const THEMES = [ + { value: 'light' as const, label: 'Light' }, + { value: 'dark' as const, label: 'Dark' }, + { value: 'ocean-light' as const, label: 'Ocean Light' }, + { value: 'ocean-dark' as const, label: 'Ocean Dark' }, + { value: 'forest-light' as const, label: 'Forest Light' }, + { value: 'forest-dark' as const, label: 'Forest Dark' }, + { value: 'sunset-light' as const, label: 'Sunset Light' }, + { value: 'sunset-dark' as const, label: 'Sunset Dark' }, + { value: 'lavender-light' as const, label: 'Lavender Light' }, + { value: 'lavender-dark' as const, label: 'Lavender Dark' }, +]; + +export function ThemeSelect() { + const { currentTheme, setTheme } = useTheme(); + + function handleThemeChange(option?: { value: string; label: string }) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy); + if (option?.value && THEMES.some((t) => t.value === option.value)) { + setTheme(option.value as ThemeName); + } + } + + return ( + + ); +} diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx deleted file mode 100644 index 23372ce..0000000 --- a/components/theme-toggle.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as Haptics from "expo-haptics"; -import { Select } from "heroui-native"; -import { Platform } from "react-native"; -import { type ThemeName, useAppTheme } from "@/lib/context/app-theme-context"; -import { Icon } from "./icon"; -import { Typography } from "./ui/typography"; - -const THEME_OPTIONS = [ - { value: "light", label: "Light", icon: "sunny" }, - { value: "dark", label: "Dark", icon: "moon" }, - { value: "catppuccin-mocha", label: "Mocha", icon: "cafe" }, - { value: "dracula", label: "Dracula", icon: "skull" }, - { value: "nord", label: "Nord", icon: "snow" }, - { value: "tokyo-night", label: "Tokyo Night", icon: "moon" }, -] as const; - -export function ThemeToggle() { - const { currentTheme, setTheme } = useAppTheme(); - - const currentThemeOption = - THEME_OPTIONS.find((option) => option.value === currentTheme) || - THEME_OPTIONS[0]; - - return ( - - ); -} diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..458e368 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,157 @@ +import { cva, type VariantProps } from 'class-variance-authority'; +import { type Href, Link } from 'expo-router'; +import type { ReactNode } from 'react'; +import { Platform, Pressable } from 'react-native'; +import { Text } from '@/components/ui/text'; +import { cn } from '@/lib/utils'; + +// NOTE: group-* is not supported yet by Uniwind + +const buttonVariants = cva( + cn( + 'group shrink-0 flex-row items-center justify-center gap-2 rounded-md shadow-none disabled:opacity-70', + Platform.select({ + web: "whitespace-nowrap outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", + }), + ), + { + variants: { + variant: { + default: cn( + 'bg-primary shadow-black/5 shadow-sm active:bg-primary/90', + Platform.select({ web: 'hover:bg-primary/90' }), + ), + destructive: cn( + 'bg-destructive shadow-black/5 shadow-sm active:bg-destructive/90 dark:bg-destructive/60', + Platform.select({ + web: 'hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40', + }), + ), + outline: cn( + 'border border-border bg-background shadow-black/5 shadow-sm active:bg-accent dark:border-input dark:bg-input/30 dark:active:bg-input/50', + Platform.select({ + web: 'hover:bg-accent dark:hover:bg-input/50', + }), + ), + secondary: cn( + 'bg-secondary shadow-black/5 shadow-sm active:bg-secondary/80', + Platform.select({ web: 'hover:bg-secondary/80' }), + ), + ghost: cn( + 'active:bg-accent dark:active:bg-accent/50', + Platform.select({ web: 'hover:bg-accent dark:hover:bg-accent/50' }), + ), + link: '', + }, + size: { + default: cn( + 'h-10 px-4 py-2 sm:h-9', + Platform.select({ web: 'has-[>svg]:px-3' }), + ), + sm: cn( + 'h-9 gap-1.5 rounded-md px-3 sm:h-8', + Platform.select({ web: 'has-[>svg]:px-2.5' }), + ), + lg: cn( + 'h-11 rounded-md px-6 sm:h-10', + Platform.select({ web: 'has-[>svg]:px-4' }), + ), + icon: 'h-10 w-10 sm:h-9 sm:w-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +const buttonTextVariants = cva( + cn( + 'font-medium text-foreground text-sm', + Platform.select({ web: 'pointer-events-none transition-colors' }), + ), + { + variants: { + variant: { + default: 'text-primary-foreground', + destructive: 'text-white', + outline: cn( + 'group-active:text-accent-foreground', + Platform.select({ web: 'group-hover:text-accent-foreground' }), + ), + secondary: 'text-secondary-foreground', + ghost: 'group-active:text-accent-foreground', + link: cn( + 'text-primary group-active:underline', + Platform.select({ + web: 'underline-offset-4 hover:underline group-hover:underline', + }), + ), + }, + size: { + default: '', + sm: '', + lg: '', + icon: '', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +type ButtonProps = Omit< + React.ComponentProps, + 'href' | 'children' +> & + React.RefAttributes & + VariantProps & { + href?: Href; + noTextWrapper?: boolean; + children?: ReactNode; + }; + +function Button({ + className, + variant, + size, + href, + noTextWrapper, + children, + ...props +}: ButtonProps) { + let content = children; + + if (!noTextWrapper) { + content = ( + + {content} + + ); + } + + content = ( + + {content} + + ); + + if (href && !props.disabled) { + content = ( + + {content} + + ); + } + + return content; +} + +export { Button, buttonTextVariants, type ButtonProps, buttonVariants }; diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..cbcf7c8 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,80 @@ +import { View, type ViewProps } from 'react-native'; +import { Text, TextClassContext } from '@/components/ui/text'; +import { cn } from '@/lib/utils'; + +function Card({ className, ...props }: ViewProps & React.RefAttributes) { + return ( + + + + ); +} + +function CardHeader({ + className, + ...props +}: ViewProps & React.RefAttributes) { + return ( + + ); +} + +function CardTitle({ + className, + ...props +}: React.ComponentProps & React.RefAttributes) { + return ( + + ); +} + +function CardDescription({ + className, + ...props +}: React.ComponentProps & React.RefAttributes) { + return ( + + ); +} + +function CardContent({ + className, + ...props +}: ViewProps & React.RefAttributes) { + return ; +} + +function CardFooter({ + className, + ...props +}: ViewProps & React.RefAttributes) { + return ( + + ); +} + +export { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +}; diff --git a/components/ui/image.tsx b/components/ui/image.tsx new file mode 100644 index 0000000..d458e45 --- /dev/null +++ b/components/ui/image.tsx @@ -0,0 +1,55 @@ +import { Image } from 'expo-image'; +import type { ImageStyle, StyleProp } from 'react-native'; +import { type AssetPath, Assets } from '@/lib/constants/assets'; +import { cn } from '@/lib/utils'; + +type ImgProps = { + src: AssetPath | (string & {}) | number | { uri: string }; + placeholder?: { + blurhash?: string; + }; + contentFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; + className?: string; + style?: StyleProp; + width?: number | `${number}%`; + height?: number | `${number}%`; + alt?: string; +}; + +function Img({ + src, + placeholder, + contentFit = 'cover', + className, + style, + width = '100%', + height, + alt, + ...props +}: ImgProps) { + let source = src; + + if (typeof src === 'string' && src.startsWith('/')) { + if (src in Assets) source = Assets[src as AssetPath]; + } + + const finalStyle: ImageStyle = { + width, + height: height ?? width, + ...((style as ImageStyle) || {}), + }; + + return ( + {alt} + ); +} + +export { Img }; diff --git a/components/ui/native-only-animated-view.tsx b/components/ui/native-only-animated-view.tsx new file mode 100644 index 0000000..30083e1 --- /dev/null +++ b/components/ui/native-only-animated-view.tsx @@ -0,0 +1,23 @@ +import { Platform } from 'react-native'; +import Animated from 'react-native-reanimated'; + +/** + * This component is used to wrap animated views that should only be animated on native. + * @param props - The props for the animated view. + * @returns The animated view if the platform is native, otherwise the children. + * @example + * + * I am only animated on native + * + */ +function NativeOnlyAnimatedView( + props: React.ComponentProps & + React.RefAttributes, +) { + if (Platform.OS === 'web') { + return <>{props.children as React.ReactNode}; + } + return ; +} + +export { NativeOnlyAnimatedView }; diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..3ff4746 --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,281 @@ +import * as SelectPrimitive from '@rn-primitives/select'; +import * as React from 'react'; +import { Platform, ScrollView, StyleSheet, View } from 'react-native'; +import { FullWindowOverlay as RNFullWindowOverlay } from 'react-native-screens'; +import { TextClassContext } from '@/components/ui/text'; +import { cn } from '@/lib/utils'; +import { Icon } from '../icon'; + +type Option = SelectPrimitive.Option; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +function SelectValue({ + ref, + className, + ...props +}: SelectPrimitive.ValueProps & + React.RefAttributes & { + className?: string; + }) { + const { value } = SelectPrimitive.useRootContext(); + return ( + + ); +} + +function SelectTrigger({ + ref, + className, + children, + icon = true, + size = 'default', + ...props +}: SelectPrimitive.TriggerProps & + React.RefAttributes & { + children?: React.ReactNode; + size?: 'default' | 'sm'; + icon?: boolean; + }) { + return ( + + {children} + {icon && ( + + )} + + ); +} + +const FullWindowOverlay = + Platform.OS === 'ios' ? RNFullWindowOverlay : React.Fragment; + +function SelectContent({ + className, + children, + position = 'popper', + portalHost, + ...props +}: SelectPrimitive.ContentProps & + React.RefAttributes & { + className?: string; + portalHost?: string; + }) { + return ( + + + + + + + + {children} + + + + + + + + ); +} + +function SelectLabel({ + className, + ...props +}: SelectPrimitive.LabelProps & React.RefAttributes) { + return ( + + ); +} + +function SelectItem({ + className, + children, + ...props +}: SelectPrimitive.ItemProps & React.RefAttributes) { + return ( + + + + + + + + + ); +} + +function SelectSeparator({ + className, + ...props +}: SelectPrimitive.SeparatorProps & + React.RefAttributes) { + return ( + + ); +} + +/** + * @platform Web only + * Returns null on native platforms + */ +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + if (Platform.OS !== 'web') { + return null; + } + return ( + + + + ); +} + +/** + * @platform Web only + * Returns null on native platforms + */ +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + if (Platform.OS !== 'web') { + return null; + } + return ( + + + + ); +} + +/** + * @platform Native only + * Returns the children on the web + */ +function NativeSelectScrollView({ + className, + ...props +}: React.ComponentProps) { + if (Platform.OS === 'web') { + return <>{props.children}; + } + return ; +} + +export { + NativeSelectScrollView, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, + type Option, +}; diff --git a/components/ui/text.tsx b/components/ui/text.tsx new file mode 100644 index 0000000..28d7f98 --- /dev/null +++ b/components/ui/text.tsx @@ -0,0 +1,95 @@ +import * as Slot from '@rn-primitives/slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from 'react'; +import { Platform, Text as RNText, type Role } from 'react-native'; +import { cn } from '@/lib/utils'; + +const textVariants = cva( + cn( + 'text-base text-foreground', + Platform.select({ + web: 'select-text', + }), + ), + { + variants: { + variant: { + default: '', + h1: cn( + 'text-center font-extrabold text-4xl tracking-tight', + Platform.select({ web: 'scroll-m-20 text-balance' }), + ), + h2: cn( + 'border-border border-b pb-2 font-semibold text-3xl tracking-tight', + Platform.select({ web: 'scroll-m-20 first:mt-0' }), + ), + h3: cn( + 'font-semibold text-2xl tracking-tight', + Platform.select({ web: 'scroll-m-20' }), + ), + h4: cn( + 'font-semibold text-xl tracking-tight', + Platform.select({ web: 'scroll-m-20' }), + ), + p: 'mt-3 leading-7 sm:mt-6', + blockquote: 'mt-4 border-l-2 pl-3 italic sm:mt-6 sm:pl-6', + code: cn( + 'relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono font-semibold text-sm', + ), + lead: 'text-muted-foreground text-xl', + large: 'font-semibold text-lg', + small: 'font-medium text-sm leading-none', + muted: 'text-muted-foreground text-sm', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +type TextVariantProps = VariantProps; + +type TextVariant = NonNullable; + +const ROLE: Partial> = { + h1: 'heading', + h2: 'heading', + h3: 'heading', + h4: 'heading', + blockquote: Platform.select({ web: 'blockquote' as Role }), + code: Platform.select({ web: 'code' as Role }), +}; + +const ARIA_LEVEL: Partial> = { + h1: '1', + h2: '2', + h3: '3', + h4: '4', +}; + +const TextClassContext = React.createContext(undefined); + +function Text({ + className, + asChild = false, + variant = 'default', + ...props +}: React.ComponentProps & + TextVariantProps & + React.RefAttributes & { + asChild?: boolean; + }) { + const textClass = React.useContext(TextClassContext); + const Component = asChild ? Slot.Text : RNText; + return ( + + ); +} + +export { Text, TextClassContext, type TextVariant, textVariants }; diff --git a/components/ui/typography.tsx b/components/ui/typography.tsx deleted file mode 100644 index 86f7292..0000000 --- a/components/ui/typography.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { cn } from "heroui-native"; -import type { ComponentProps } from "react"; -import { Text } from "react-native"; -import { tv, type VariantProps } from "tailwind-variants"; - -const typographyVariants = tv({ - base: "text-foreground", - variants: { - variant: { - h1: "text-4xl font-bold tracking-tight lg:text-5xl", - h2: "text-3xl font-semibold tracking-tight", - h3: "text-2xl font-semibold tracking-tight", - h4: "text-xl font-semibold tracking-tight", - h5: "text-lg font-semibold", - h6: "text-base font-semibold", - p: "leading-7 [&:not(:first-child)]:mt-6", - blockquote: "mt-6 border-l-2 border-border pl-6 italic", - "inline-code": - "relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold", - lead: "text-xl text-muted-foreground", - large: "text-lg font-semibold", - small: "text-sm font-medium leading-none", - muted: "text-sm text-muted-foreground", - }, - }, - defaultVariants: { - variant: "p", - }, -}); - -type TypographyProps = ComponentProps & - VariantProps; - -export function Typography({ className, variant, ...props }: TypographyProps) { - return ( - - ); -} diff --git a/eas.json b/eas.json index a69131c..a18b424 100644 --- a/eas.json +++ b/eas.json @@ -1,6 +1,7 @@ { "cli": { - "version": ">= 7.3.0" + "version": ">= 7.3.0", + "appVersionSource": "remote" }, "build": { "development": { @@ -10,7 +11,9 @@ "preview": { "distribution": "internal" }, - "production": {} + "production": { + "autoIncrement": true + } }, "submit": { "production": {} diff --git a/lib/config/app.ts b/lib/config/app.ts new file mode 100644 index 0000000..a4cfb63 --- /dev/null +++ b/lib/config/app.ts @@ -0,0 +1,6 @@ +import { deepmerge } from '@ts-utilities/core'; +import { config } from '../../app.config'; + +export const appConfig = deepmerge(config, { + env: __DEV__, +}); diff --git a/lib/config/ui.ts b/lib/config/ui.ts deleted file mode 100644 index 6aea5aa..0000000 --- a/lib/config/ui.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { HeroUINativeConfig } from "heroui-native"; - -export const uiConfig: HeroUINativeConfig = { - toast: { - defaultProps: { - placement: "top", - }, - maxVisibleToasts: 2, - }, -}; diff --git a/lib/constants/assets.ts b/lib/constants/assets.ts new file mode 100644 index 0000000..4721189 --- /dev/null +++ b/lib/constants/assets.ts @@ -0,0 +1,17 @@ +/** + * ⚠️ AUTO-GENERATED FILE + * Do not edit manually. + */ + +export const Assets = { + '/favicon.png': require('@/assets/favicon.png'), + '/icons/adaptive-icon.png': require('@/assets/icons/adaptive-icon.png'), + '/icons/ios-dark.png': require('@/assets/icons/ios-dark.png'), + '/icons/ios-light.png': require('@/assets/icons/ios-light.png'), + '/icons/ios-tinted.png': require('@/assets/icons/ios-tinted.png'), + '/icons/splash-icon-dark.png': require('@/assets/icons/splash-icon-dark.png'), + '/icons/splash-icon-light.png': require('@/assets/icons/splash-icon-light.png'), + '/logo.png': require('@/assets/logo.png'), +} as const; + +export type AssetPath = keyof typeof Assets; diff --git a/lib/context/app-theme-context.tsx b/lib/context/app-theme-context.tsx deleted file mode 100644 index 2a7b48d..0000000 --- a/lib/context/app-theme-context.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type React from "react"; -import { createContext, useCallback, useContext, useMemo } from "react"; -import { Uniwind, useUniwind } from "uniwind"; - -export type ThemeName = ReturnType["theme"]; - -type AppThemeContextType = { - currentTheme: ThemeName; - setTheme: (theme: ThemeName) => void; - toggleTheme: () => void; -}; - -const AppThemeContext = createContext( - undefined, -); - -export const AppThemeProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - const { theme } = useUniwind(); - - const setTheme = useCallback((newTheme: ThemeName) => { - Uniwind.setTheme(newTheme); - }, []); - - const toggleTheme = useCallback(() => { - Uniwind.setTheme(theme === "light" ? "dark" : "light"); - }, [theme]); - - const value = useMemo( - () => ({ - currentTheme: theme, - setTheme, - toggleTheme, - }), - [theme, setTheme, toggleTheme], - ); - - return ( - - {children} - - ); -}; - -export function useAppTheme() { - const context = useContext(AppThemeContext); - if (!context) { - throw new Error("useAppTheme must be used within AppThemeProvider"); - } - return context; -} diff --git a/lib/context/providers.tsx b/lib/context/providers.tsx index b575cc4..9e21b79 100644 --- a/lib/context/providers.tsx +++ b/lib/context/providers.tsx @@ -1,38 +1,31 @@ -import { HeroUINativeProvider } from "heroui-native"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; -import { KeyboardProvider } from "react-native-keyboard-controller"; -import { uiConfig } from "../config/ui"; -import { ReactQuery } from "../trpc/react-query"; -import { AppThemeProvider } from "./app-theme-context"; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { KeyboardProvider } from 'react-native-keyboard-controller'; +import { SafeAreaListener } from 'react-native-safe-area-context'; +import { Uniwind } from 'uniwind'; +import { ReactQuery } from '../trpc/react-query'; +import { stackProviders } from '../utils/stack-provider'; +import { ThemeProvider } from './theme'; -function CoreProviders({ children }: { children: React.ReactNode }) { - return ( - +export const Providers = stackProviders([ + function GestureHandler({ children }) { + return ( - {children} + {children} - - ); -} - -export function Providers({ - children, - onlyCoreProviders, -}: { - children: React.ReactNode; - onlyCoreProviders?: boolean; -}) { - if (onlyCoreProviders) { - return {children}; - } - - return ( - - - - {children} - - - - ); -} + ); + }, + function SafeAreaHandler({ children }) { + return ( + { + Uniwind.updateInsets(insets); + }} + > + {children} + + ); + }, + ThemeProvider, + KeyboardProvider, + ReactQuery, +]); diff --git a/lib/context/theme.tsx b/lib/context/theme.tsx new file mode 100644 index 0000000..119e204 --- /dev/null +++ b/lib/context/theme.tsx @@ -0,0 +1,112 @@ +import * as SplashScreen from 'expo-splash-screen'; +import * as React from 'react'; +import { StatusBar } from 'react-native'; +import { Uniwind, useCSSVariable, useUniwind } from 'uniwind'; +import { useStorageState } from '../hooks/useStorageState'; + +export type ThemeName = ReturnType['theme']; + +const COLORS = [ + 'foreground', + 'background', + 'accent', + 'muted', + 'primary', + 'primary-foreground', + 'secondary', + 'secondary-foreground', + 'card', + 'card-foreground', + 'popover', + 'popover-foreground', + 'destructive', + 'border', + 'input', + 'ring', +] as const; + +type ThemeContextType = { + currentTheme: ThemeName; + setTheme: (theme: ThemeName) => void; + colorScheme: 'light' | 'dark'; + colors: { + [key in (typeof COLORS)[number]]: string; + }; +}; + +const ThemeContext = React.createContext( + undefined, +); + +export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { + const { theme: defaultValue } = useUniwind(); + const [storedTheme, setStoredTheme] = useStorageState( + 'app-theme', + { + defaultValue, + }, + ); + + const colorValues = useCSSVariable(COLORS.map((c) => `--color-${c}`)); + + const colorScheme: ThemeContextType['colorScheme'] = React.useMemo( + () => (storedTheme?.includes('dark') ? 'dark' : 'light'), + [storedTheme], + ); + + const colors = React.useMemo( + () => + COLORS.reduce( + (acc, key, index) => { + const value = colorValues[index]; + + acc[key] = typeof value === 'string' ? value : String(value ?? ''); + + return acc; + }, + {} as ThemeContextType['colors'], + ), + [colorValues], + ); + + const setTheme = React.useCallback( + async (newTheme: ThemeName) => { + await setStoredTheme(newTheme); + }, + [setStoredTheme], + ); + + React.useEffect(() => { + if (storedTheme) { + Uniwind.setTheme(storedTheme); + + StatusBar.setBarStyle( + `${colorScheme === 'dark' ? 'light' : 'dark'}-content`, + ); + + SplashScreen.hide(); + } + }, [storedTheme, colorScheme]); + + const value = React.useMemo( + () => ({ + currentTheme: storedTheme, + setTheme, + colors, + colorScheme, + }), + [storedTheme, setTheme, colors, colorScheme], + ); + + return ( + {children} + ); +}; + +export function useTheme() { + const context = React.useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within ThemeProvider'); + } + return context; +} diff --git a/lib/hooks/useStorageState.ts b/lib/hooks/useStorageState.ts new file mode 100644 index 0000000..dba99ab --- /dev/null +++ b/lib/hooks/useStorageState.ts @@ -0,0 +1,85 @@ +import * as SecureStore from 'expo-secure-store'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +type DefaultValue = T | (() => T) | (() => Promise); + +type UseStorageStateOptions = { + defaultValue: DefaultValue; +}; + +export function useStorageState( + key: string, + opts: UseStorageStateOptions, +): [T, (newValue: T | ((prev: T) => T)) => Promise] { + const { defaultValue } = opts; + const [value, setValue] = useState(); + const [isInitialized, setIsInitialized] = useState(false); + const initializingRef = useRef(false); + const defaultValueRef = useRef(defaultValue); + const isSavingRef = useRef(false); + + defaultValueRef.current = defaultValue; + + const resolveDefaultValue = useCallback(async (): Promise => { + const def = defaultValueRef.current; + if (typeof def === 'function') { + const result = (def as () => T | Promise)(); + return result instanceof Promise ? await result : result; + } + return def; + }, []); + + useEffect(() => { + if (initializingRef.current) return; + initializingRef.current = true; + + async function loadValue() { + try { + const storedValue = await SecureStore.getItemAsync(key); + setValue( + storedValue !== null + ? (JSON.parse(storedValue) as T) + : await resolveDefaultValue(), + ); + } catch (error) { + console.info(`⚡[useStorageState.ts:${key}] GET error:`, error); + setValue(await resolveDefaultValue()); + } finally { + setIsInitialized(true); + } + } + loadValue(); + }, [key, resolveDefaultValue]); + + const updateValue = useCallback( + async (newValue: T | ((prev: T) => T)) => { + setValue((prevValue) => { + if (!isInitialized || prevValue === undefined) return prevValue; + + const nextValue = + typeof newValue === 'function' + ? (newValue as (prev: T) => T)(prevValue) + : newValue; + + if (isSavingRef.current) return nextValue; + + isSavingRef.current = true; + + SecureStore.setItemAsync(key, JSON.stringify(nextValue)) + .catch((error) => { + console.info(`⚡[useStorageState.ts:${key}] SAVE error:`, error); + setValue(prevValue); + throw error; + }) + .finally(() => { + isSavingRef.current = false; + }); + + return nextValue; + }); + }, + [key, isInitialized], + ); + + return [value as T, updateValue]; +} diff --git a/lib/scripts/generate-assets.ts b/lib/scripts/generate-assets.ts new file mode 100644 index 0000000..48dba22 --- /dev/null +++ b/lib/scripts/generate-assets.ts @@ -0,0 +1,125 @@ +#!/usr/bin/env bun + +import type { Stats } from 'node:fs'; +import { mkdir, readdir, stat, writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +const PROJECT_ROOT = path.resolve(__dirname, '../..'); +const ASSETS_DIR = path.join(PROJECT_ROOT, 'assets'); +const OUTPUT_FILE = path.join(PROJECT_ROOT, 'lib', 'constants', 'assets.ts'); + +const VALID_EXTENSIONS = new Set([ + '.png', + '.jpg', + '.jpeg', + '.webp', + '.gif', + '.avif', + '.svg', +]); + +type AssetEntry = { + key: string; + requirePath: string; +}; + +async function walk(dir: string): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + const files: string[] = []; + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + files.push(...(await walk(fullPath))); + } else if (entry.isFile()) { + files.push(fullPath); + } + } + + return files; +} + +function normalizeSlash(p: string) { + return p.replace(/\\/g, '/'); +} + +function assertSafeKey(key: string) { + if (!key.startsWith('/')) { + throw new Error(`Asset key must start with "/": ${key}`); + } + + if (key.includes(' ')) { + throw new Error(`Asset path contains spaces (not recommended): ${key}`); + } +} + +async function main() { + try { + // 1️⃣ Ensure assets folder exists + let stats: Stats; + try { + stats = await stat(ASSETS_DIR); + } catch { + throw new Error(`Assets folder not found: ${ASSETS_DIR}`); + } + + if (!stats.isDirectory()) { + throw new Error(`Assets path is not a directory: ${ASSETS_DIR}`); + } + + // 2️⃣ Walk assets + const allFiles = await walk(ASSETS_DIR); + + const assets: AssetEntry[] = []; + + for (const file of allFiles) { + const ext = path.extname(file).toLowerCase(); + if (!VALID_EXTENSIONS.has(ext)) continue; + + const relativeToAssets = path.relative(ASSETS_DIR, file); + const slashRelative = normalizeSlash(relativeToAssets); + + const key = `/${slashRelative}`; + assertSafeKey(key); + + const requirePath = `@/assets/${slashRelative}`; + + assets.push({ key, requirePath }); + } + + if (assets.length === 0) { + console.warn('⚠️ No valid asset files found.'); + } + + // 3️⃣ Sort for stable output + assets.sort((a, b) => a.key.localeCompare(b.key)); + + // 4️⃣ Ensure output directory exists + await mkdir(path.dirname(OUTPUT_FILE), { recursive: true }); + + // 5️⃣ Generate file + const content = `/** + * ⚠️ AUTO-GENERATED FILE + * Do not edit manually. + */ + +export const Assets = { +${assets.map((a) => ` '${a.key}': require('${a.requirePath}'),`).join('\n')} +} as const; + +export type AssetPath = keyof typeof Assets; +`; + + await writeFile(OUTPUT_FILE, content, 'utf8'); + + console.log(`✅ Generated ${assets.length} assets`); + console.log(`📄 Output: ${path.relative(PROJECT_ROOT, OUTPUT_FILE)}`); + } catch (err) { + console.error('❌ Asset generation failed'); + console.error(err instanceof Error ? err.message : err); + process.exit(1); + } +} + +main(); diff --git a/lib/trpc/api.ts b/lib/trpc/api.ts index 1cc1475..c35a0b9 100644 --- a/lib/trpc/api.ts +++ b/lib/trpc/api.ts @@ -1,4 +1,4 @@ -import { createTRPCReact } from "@trpc/react-query"; -import type { AppRouter } from "../../server/root"; +import { createTRPCReact } from '@trpc/react-query'; +import type { AppRouter } from '../../server/root'; export const api = createTRPCReact({}); diff --git a/lib/trpc/query-client.tsx b/lib/trpc/query-client.tsx index f9daa86..589fd59 100644 --- a/lib/trpc/query-client.tsx +++ b/lib/trpc/query-client.tsx @@ -1,3 +1,3 @@ -import { QueryClient } from "@tanstack/react-query"; +import { QueryClient } from '@tanstack/react-query'; export const createQueryClient = () => new QueryClient({}); diff --git a/lib/trpc/react-query.tsx b/lib/trpc/react-query.tsx index d5226c5..26d78a8 100644 --- a/lib/trpc/react-query.tsx +++ b/lib/trpc/react-query.tsx @@ -1,13 +1,13 @@ -"use client"; +'use client'; -import { type QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { createQueryClient } from "./query-client"; -import { TRPCReactProvider } from "./trpc-provider"; +import { type QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createQueryClient } from './query-client'; +import { TRPCReactProvider } from './trpc-provider'; let clientQueryClientSingleton: QueryClient | undefined; const getQueryClient = () => { - if (typeof window === "undefined") { + if (typeof window === 'undefined') { // Server: always make a new query client return createQueryClient(); } diff --git a/lib/trpc/trpc-provider.tsx b/lib/trpc/trpc-provider.tsx index d322c83..418b04a 100644 --- a/lib/trpc/trpc-provider.tsx +++ b/lib/trpc/trpc-provider.tsx @@ -1,8 +1,8 @@ -import type { QueryClient } from "@tanstack/react-query"; -import { httpBatchLink, loggerLink } from "@trpc/client"; -import { useState } from "react"; -import { transformer } from "../utils"; -import { api } from "./api"; +import type { QueryClient } from '@tanstack/react-query'; +import { httpBatchLink } from '@trpc/client'; +import { useState } from 'react'; +import { transformer } from '../utils'; +import { api } from './api'; export function TRPCReactProvider({ children, @@ -14,16 +14,11 @@ export function TRPCReactProvider({ const [trpcClient] = useState(() => api.createClient({ links: [ - loggerLink({ - enabled: (op) => - process.env.NODE_ENV === "development" || - (op.direction === "down" && op.result instanceof Error), - }), httpBatchLink({ transformer, url: process.env.EXPO_PUBLIC_TRPC_SERVER || - "https://native-template.vercel.app/trpc", + 'https://native-template.vercel.app/trpc', }), ], }), diff --git a/lib/utils/env.ts b/lib/utils/env.ts index b60cd81..680cd3c 100644 --- a/lib/utils/env.ts +++ b/lib/utils/env.ts @@ -1,8 +1,8 @@ -import { Platform } from "react-native"; +import { Platform } from 'react-native'; export function getEnv() { return { - env: __DEV__ ? "development" : "production", + env: __DEV__ ? 'development' : 'production', os: Platform.OS, }; } diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 1e84e8f..86793ed 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -1,3 +1,9 @@ -import superjson from "superjson"; +import { type ClassValue, clsx } from 'clsx'; +import superjson from 'superjson'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} export const transformer = superjson; diff --git a/lib/utils/stack-provider.tsx b/lib/utils/stack-provider.tsx new file mode 100644 index 0000000..577b34e --- /dev/null +++ b/lib/utils/stack-provider.tsx @@ -0,0 +1,13 @@ +type ProviderType = React.ComponentType<{ children: React.ReactNode }>; + +export function stackProviders(providers: ProviderType[]) { + return function Providers({ children }: { children: React.ReactNode }) { + return providers.reduceRight((acc, Provider, idx) => { + return ( + + {acc} + + ); + }, children); + }; +} diff --git a/lib/utils/swipe-gesture-handler.ts b/lib/utils/swipe-gesture-handler.ts new file mode 100644 index 0000000..43b1a88 --- /dev/null +++ b/lib/utils/swipe-gesture-handler.ts @@ -0,0 +1,73 @@ +import { Gesture } from 'react-native-gesture-handler'; +import { scheduleOnRN } from 'react-native-worklets'; + +export type SwipeDirection = 'left' | 'right' | 'up' | 'down'; + +export type SwipeGestureOptions = { + threshold?: number; + velocityThreshold?: number; + lockAxis?: boolean; + + onLeft?: () => void; // from right to left + onRight?: () => void; // from left to right + onUp?: () => void; // from bottom to top + onDown?: () => void; // from top to bottom +}; + +export function swipeGesture({ + threshold = 50, + velocityThreshold = 0, + lockAxis = true, + onLeft, + onRight, + onUp, + onDown, +}: SwipeGestureOptions) { + return Gesture.Pan().onEnd((e) => { + 'worklet'; + + const x = e.translationX; + const y = e.translationY; + + const absX = x < 0 ? -x : x; + const absY = y < 0 ? -y : y; + + // NOTE: Axis-locked (fast path) + if (lockAxis) { + if (absX > absY) { + if (absX < threshold) return; + if (velocityThreshold && Math.abs(e.velocityX) < velocityThreshold) + return; + + if (x > 0 && onRight) scheduleOnRN(onRight); + else if (x < 0 && onLeft) scheduleOnRN(onLeft); + return; + } + + if (absY < threshold) return; + if (velocityThreshold && Math.abs(e.velocityY) < velocityThreshold) + return; + + if (y > 0 && onDown) scheduleOnRN(onDown); + else if (y < 0 && onUp) scheduleOnRN(onUp); + return; + } + + // NOTE: No axis lock + if ( + absX >= threshold && + (!velocityThreshold || Math.abs(e.velocityX) >= velocityThreshold) + ) { + if (x > 0 && onRight) scheduleOnRN(onRight); + else if (x < 0 && onLeft) scheduleOnRN(onLeft); + } + + if ( + absY >= threshold && + (!velocityThreshold || Math.abs(e.velocityY) >= velocityThreshold) + ) { + if (y > 0 && onDown) scheduleOnRN(onDown); + else if (y < 0 && onUp) scheduleOnRN(onUp); + } + }); +} diff --git a/metro.config.js b/metro.config.js index 114b7a9..ae17d40 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,13 +1,22 @@ -const { getDefaultConfig } = require("expo/metro-config"); -const { withUniwindConfig } = require("uniwind/metro"); +const { getDefaultConfig } = require('expo/metro-config'); +const { withUniwindConfig } = require('uniwind/metro'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); const uniwindConfig = withUniwindConfig(config, { - cssEntryFile: "./styles/global.css", - dtsFile: "./types/uniwind-types.d.ts", - extraThemes: ["nord", "dracula", "tokyo-night", "catppuccin-mocha"], + cssEntryFile: './styles/global.css', + dtsFile: './types/uniwind-types.d.ts', + extraThemes: [ + 'ocean-light', + 'ocean-dark', + 'forest-light', + 'forest-dark', + 'sunset-light', + 'sunset-dark', + 'lavender-light', + 'lavender-dark', + ], }); module.exports = uniwindConfig; diff --git a/package.json b/package.json index 1d93eee..0010678 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,29 @@ { "name": "native-template", - "version": "1.0.0", "private": true, + "version": "1.0.0", "main": "expo-router/entry", "scripts": { - "check": "biome check --write .", "start": "expo start", "dev": "expo start --clear", - "test": "vitest", "android": "expo run:android", "healthcheck": "npx expo-doctor", "ios": "expo run:ios", "web": "expo start --web", + "check": "biome check .", + "fix": "biome check --diagnostic-level=error --write .", + "lint": "biome lint .", + "lint:fix": "biome lint --write --unsafe .", + "format": "biome format .", + "format:write": "biome format . --write", + "typecheck": "tsgo --noEmit", + "test": "vitest", + "test:run": "vitest run", + "test:ui": "vitest --ui", + "test:log": "vitest run --reporter verbose", + "generate:assets": "bun lib/scripts/generate-assets.ts", + "predev": "bun run generate:assets", + "prebuild": "bun run predev", "prepare": "husky" }, "dependencies": { @@ -20,24 +32,29 @@ "@gorhom/bottom-sheet": "^5.2.8", "@react-navigation/drawer": "^7.5.0", "@react-navigation/elements": "^2.9.3", + "@react-navigation/native": "^7.1.26", + "@rn-primitives/portal": "^1.3.0", + "@rn-primitives/select": "^1.2.0", + "@rn-primitives/slot": "^1.2.0", "@tanstack/react-query": "^5.90.12", "@trpc/client": "^11.8.1", "@trpc/react-query": "^11.8.1", "@trpc/server": "^11.8.1", "@trpc/tanstack-react-query": "^11.8.1", "@ts-utilities/core": "^1.0.6", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "expo": "^54.0.30", "expo-constants": "~18.0.12", "expo-font": "~14.0.10", "expo-haptics": "^15.0.8", + "expo-image": "^3.0.11", "expo-linking": "~8.0.11", "expo-network": "~8.0.8", "expo-router": "^6.0.21", "expo-secure-store": "~15.0.8", "expo-server": "^1.0.5", "expo-splash-screen": "^31.0.13", - "expo-status-bar": "~3.0.9", - "heroui-native": "1.0.0-beta.9", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.5", @@ -49,8 +66,7 @@ "react-native-svg": "15.12.1", "react-native-web": "^0.21.2", "react-native-worklets": "0.5.1", - "superjson": "^2.2.6", - "zod": "^4.2.1" + "superjson": "^2.2.6" }, "devDependencies": { "@biomejs/biome": "^2.3.10", @@ -59,13 +75,18 @@ "@types/react": "~19.1.10", "husky": "^9.1.7", "tailwind-merge": "^3.4.0", - "tailwind-variants": "3.2.2", "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.4.0", "typescript": "~5.9.2", "uniwind": "^1.2.2", - "vitest": "^4.0.16" + "vitest": "^4.0.16", + "zod": "^4.2.1" }, "engines": { "node": "22.x" - } + }, + "author": "Sohan Emon ", + "description": "A template for creating native apps with Expo and React Native", + "icon": "/assets/favicon.png", + "license": "MIT" } diff --git a/server/root.ts b/server/root.ts index ac27052..84d110a 100644 --- a/server/root.ts +++ b/server/root.ts @@ -1,5 +1,5 @@ -import { healthcheckRouter } from "./routers/healthcheck"; -import { createTRPCRouter } from "./trpc"; +import { healthcheckRouter } from './routers/healthcheck'; +import { createTRPCRouter } from './trpc'; /** * This is the primary router for your server. diff --git a/server/routers/healthcheck.ts b/server/routers/healthcheck.ts index c570f22..268b001 100644 --- a/server/routers/healthcheck.ts +++ b/server/routers/healthcheck.ts @@ -1,9 +1,9 @@ -import { createTRPCRouter, publicProcedure } from "../trpc"; +import { createTRPCRouter, publicProcedure } from '../trpc'; export const healthcheckRouter = createTRPCRouter({ check: publicProcedure.query(() => { return { - status: "OK", + status: 'OK', timestamp: new Date().toISOString(), }; }), diff --git a/server/routers/post.ts b/server/routers/post.ts index 5286f5b..761fd2e 100644 --- a/server/routers/post.ts +++ b/server/routers/post.ts @@ -1,5 +1,5 @@ -import { z } from "zod"; -import { createTRPCRouter, publicProcedure } from "../trpc"; +import { z } from 'zod'; +import { createTRPCRouter, publicProcedure } from '../trpc'; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); diff --git a/server/trpc.ts b/server/trpc.ts index 7229284..bf95964 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -6,9 +6,9 @@ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will * need to use are documented accordingly near the end. */ -import { initTRPC } from "@trpc/server"; -import superjson from "superjson"; -import { ZodError } from "zod"; +import { initTRPC } from '@trpc/server'; +import superjson from 'superjson'; +import { ZodError } from 'zod'; // import { db } from "~/server/db"; const db = {}; diff --git a/sohanscript.json b/sohanscript.json new file mode 100644 index 0000000..93c1bfb --- /dev/null +++ b/sohanscript.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://sohanjs.web.app/draft/next/schema.json", + "packageManager": "bun", + "cwd": "$NVIM_CWD", + "commands": { + "dev": "bun run dev" + } +} diff --git a/styles/colors.css b/styles/colors.css index ee09517..223ecae 100644 --- a/styles/colors.css +++ b/styles/colors.css @@ -1,129 +1,334 @@ @layer theme { :root { @variant light { - --background: oklch(0.9911 0 0); - --foreground: oklch(0.2046 0 0); + --color-background: oklch(1 0 0); + --color-foreground: oklch(0.145 0 0); + --color-card: oklch(1 0 0); + --color-card-foreground: oklch(0.145 0 0); + --color-popover: oklch(1 0 0); + --color-popover-foreground: oklch(0.145 0 0); + --color-primary: oklch(0.205 0 0); + --color-primary-foreground: oklch(0.985 0 0); + --color-secondary: oklch(0.97 0 0); + --color-secondary-foreground: oklch(0.205 0 0); + --color-muted: oklch(0.97 0 0); + --color-muted-foreground: oklch(0.556 0 0); + --color-accent: oklch(0.97 0 0); + --color-accent-foreground: oklch(0.205 0 0); + --color-destructive: oklch(0.577 0.245 27.325); + --color-border: oklch(0.922 0 0); + --color-input: oklch(0.922 0 0); + --color-ring: oklch(0.708 0 0); + --color-chart-1: oklch(0.646 0.222 41.116); + --color-chart-2: oklch(0.6 0.118 184.704); + --color-chart-3: oklch(0.398 0.07 227.392); + --color-chart-4: oklch(0.828 0.189 84.429); + --color-chart-5: oklch(0.769 0.188 70.08); + --color-sidebar: oklch(0.985 0 0); + --color-sidebar-foreground: oklch(0.145 0 0); + --color-sidebar-primary: oklch(0.205 0 0); + --color-sidebar-primary-foreground: oklch(0.985 0 0); + --color-sidebar-accent: oklch(0.97 0 0); + --color-sidebar-accent-foreground: oklch(0.205 0 0); + --color-sidebar-border: oklch(0.922 0 0); + --color-sidebar-ring: oklch(0.708 0 0); } @variant dark { - --background: oklch(0.1822 0 0); - --foreground: oklch(0.9288 0.0126 255.5078); + --color-background: oklch(0.145 0 0); + --color-foreground: oklch(0.985 0 0); + --color-card: oklch(0.205 0 0); + --color-card-foreground: oklch(0.985 0 0); + --color-popover: oklch(0.205 0 0); + --color-popover-foreground: oklch(0.985 0 0); + --color-primary: oklch(0.922 0 0); + --color-primary-foreground: oklch(0.205 0 0); + --color-secondary: oklch(0.269 0 0); + --color-secondary-foreground: oklch(0.985 0 0); + --color-muted: oklch(0.269 0 0); + --color-muted-foreground: oklch(0.708 0 0); + --color-accent: oklch(0.269 0 0); + --color-accent-foreground: oklch(0.985 0 0); + --color-destructive: oklch(0.704 0.191 22.216); + --color-border: oklch(1 0 0 / 10%); + --color-input: oklch(1 0 0 / 15%); + --color-ring: oklch(0.556 0 0); + --color-chart-1: oklch(0.488 0.243 264.376); + --color-chart-2: oklch(0.696 0.17 162.48); + --color-chart-3: oklch(0.769 0.188 70.08); + --color-chart-4: oklch(0.627 0.265 303.9); + --color-chart-5: oklch(0.645 0.246 16.439); + --color-sidebar: oklch(0.205 0 0); + --color-sidebar-foreground: oklch(0.985 0 0); + --color-sidebar-primary: oklch(0.488 0.243 264.376); + --color-sidebar-primary-foreground: oklch(0.985 0 0); + --color-sidebar-accent: oklch(0.269 0 0); + --color-sidebar-accent-foreground: oklch(0.985 0 0); + --color-sidebar-border: oklch(1 0 0 / 10%); + --color-sidebar-ring: oklch(0.556 0 0); } - - /* Catppuccin Mocha (dark) */ - @variant catppuccin-mocha { - --background: oklch(0.0706 0.003 95.84); - --foreground: oklch(0.949 0.004 95.84); - --surface: oklch(0.0863 0.004 95.84); - --surface-foreground: oklch(0.949 0.004 95.84); - --overlay: oklch(0.1176 0.005 95.84); - --overlay-foreground: oklch(0.949 0.004 95.84); - --muted: oklch(0.5882 0.006 95.84); - --default: oklch(0.0863 0.004 95.84); - --default-foreground: oklch(0.949 0.004 95.84); - --accent: oklch(0.5882 0.169 263.02); - --accent-foreground: oklch(0.0706 0.003 95.84); - --success: oklch(0.6471 0.131 142.5); - --success-foreground: oklch(0.0706 0.003 95.84); - --warning: oklch(0.7647 0.129 85.87); - --warning-foreground: oklch(0.0706 0.003 95.84); - --danger: oklch(0.6118 0.153 25.26); - --danger-foreground: oklch(0.0706 0.003 95.84); - --field-background: oklch(0.0863 0.004 95.84); - --field-foreground: oklch(0.949 0.004 95.84); - --field-placeholder: oklch(0.5882 0.006 95.84); - --field-border: oklch(0.1451 0.005 95.84); - --segment: oklch(0.1451 0.005 95.84); - --segment-foreground: oklch(0.949 0.004 95.84); - --border: oklch(0.1451 0.005 95.84); - --divider: oklch(0.1451 0.005 95.84); - --link: oklch(0.5882 0.169 263.02); + @variant ocean-light { + --color-background: oklch(0.98 0.02 220); + --color-foreground: oklch(0.2 0.08 230); + --color-card: oklch(0.98 0.02 220); + --color-card-foreground: oklch(0.2 0.08 230); + --color-popover: oklch(0.98 0.02 220); + --color-popover-foreground: oklch(0.2 0.08 230); + --color-primary: oklch(0.45 0.15 250); + --color-primary-foreground: oklch(0.98 0.02 220); + --color-secondary: oklch(0.92 0.06 220); + --color-secondary-foreground: oklch(0.2 0.08 230); + --color-muted: oklch(0.95 0.03 220); + --color-muted-foreground: oklch(0.5 0.08 230); + --color-accent: oklch(0.95 0.03 220); + --color-accent-foreground: oklch(0.2 0.08 230); + --color-destructive: oklch(0.577 0.245 27.325); + --color-border: oklch(0.88 0.04 220); + --color-input: oklch(0.88 0.04 220); + --color-ring: oklch(0.45 0.15 250); + --color-chart-1: oklch(0.55 0.18 250); + --color-chart-2: oklch(0.6 0.12 190); + --color-chart-3: oklch(0.65 0.1 160); + --color-chart-4: oklch(0.7 0.15 280); + --color-chart-5: oklch(0.6 0.14 210); + --color-sidebar: oklch(0.96 0.02 220); + --color-sidebar-foreground: oklch(0.2 0.08 230); + --color-sidebar-primary: oklch(0.45 0.15 250); + --color-sidebar-primary-foreground: oklch(0.98 0.02 220); + --color-sidebar-accent: oklch(0.95 0.03 220); + --color-sidebar-accent-foreground: oklch(0.2 0.08 230); + --color-sidebar-border: oklch(0.88 0.04 220); + --color-sidebar-ring: oklch(0.45 0.15 250); } - /* Dracula (dark) */ - @variant dracula { - --background: oklch(0.16 0.01 250); - --foreground: oklch(0.96 0.005 90); - --surface: oklch(0.27 0.01 245); - --surface-foreground: oklch(0.96 0.005 90); - --overlay: oklch(0.35 0.015 245); - --overlay-foreground: oklch(0.96 0.005 90); - --muted: oklch(0.55 0.08 240); - --default: oklch(0.27 0.01 245); - --default-foreground: oklch(0.96 0.005 90); - --accent: oklch(0.67 0.2 25); - --accent-foreground: oklch(0.16 0.01 250); - --success: oklch(0.8 0.18 145); - --success-foreground: oklch(0.16 0.01 250); - --warning: oklch(0.93 0.18 95); - --warning-foreground: oklch(0.16 0.01 250); - --danger: oklch(0.67 0.2 25); - --danger-foreground: oklch(0.16 0.01 250); - --field-background: oklch(0.27 0.01 245); - --field-foreground: oklch(0.96 0.005 90); - --field-placeholder: oklch(0.55 0.08 240); - --field-border: oklch(0.35 0.015 245); - --segment: oklch(0.35 0.015 245); - --segment-foreground: oklch(0.96 0.005 90); - --border: oklch(0.35 0.015 245); - --divider: oklch(0.35 0.015 245); - --link: oklch(0.67 0.2 25); + @variant ocean-dark { + --color-background: oklch(0.15 0.05 230); + --color-foreground: oklch(0.96 0.01 220); + --color-card: oklch(0.2 0.06 230); + --color-card-foreground: oklch(0.96 0.01 220); + --color-popover: oklch(0.2 0.06 230); + --color-popover-foreground: oklch(0.96 0.01 220); + --color-primary: oklch(0.65 0.2 250); + --color-primary-foreground: oklch(0.15 0.05 230); + --color-secondary: oklch(0.28 0.08 230); + --color-secondary-foreground: oklch(0.96 0.01 220); + --color-muted: oklch(0.3 0.06 230); + --color-muted-foreground: oklch(0.65 0.05 220); + --color-accent: oklch(0.3 0.06 230); + --color-accent-foreground: oklch(0.96 0.01 220); + --color-destructive: oklch(0.704 0.191 22.216); + --color-border: oklch(0.35 0.08 230); + --color-input: oklch(0.35 0.08 230); + --color-ring: oklch(0.65 0.2 250); + --color-chart-1: oklch(0.7 0.2 250); + --color-chart-2: oklch(0.75 0.15 190); + --color-chart-3: oklch(0.8 0.12 160); + --color-chart-4: oklch(0.75 0.18 280); + --color-chart-5: oklch(0.7 0.16 210); + --color-sidebar: oklch(0.2 0.06 230); + --color-sidebar-foreground: oklch(0.96 0.01 220); + --color-sidebar-primary: oklch(0.65 0.2 250); + --color-sidebar-primary-foreground: oklch(0.15 0.05 230); + --color-sidebar-accent: oklch(0.3 0.06 230); + --color-sidebar-accent-foreground: oklch(0.96 0.01 220); + --color-sidebar-border: oklch(0.35 0.08 230); + --color-sidebar-ring: oklch(0.65 0.2 250); } - /* Nord (dark) */ - @variant nord { - --background: oklch(0.18 0.008 220); - --foreground: oklch(0.85 0.01 220); - --surface: oklch(0.24 0.01 220); - --surface-foreground: oklch(0.85 0.01 220); - --overlay: oklch(0.28 0.01 220); - --overlay-foreground: oklch(0.85 0.01 220); - --muted: oklch(0.55 0.08 220); - --default: oklch(0.24 0.01 220); - --default-foreground: oklch(0.85 0.01 220); - --accent: oklch(0.75 0.03 180); - --accent-foreground: oklch(0.18 0.008 220); - --success: oklch(0.72 0.08 110); - --success-foreground: oklch(0.18 0.008 220); - --warning: oklch(0.85 0.12 75); - --warning-foreground: oklch(0.18 0.008 220); - --danger: oklch(0.58 0.1 10); - --danger-foreground: oklch(0.18 0.008 220); - --field-background: oklch(0.24 0.01 220); - --field-foreground: oklch(0.85 0.01 220); - --field-placeholder: oklch(0.55 0.08 220); - --field-border: oklch(0.33 0.01 220); - --segment: oklch(0.33 0.01 220); - --segment-foreground: oklch(0.85 0.01 220); - --border: oklch(0.33 0.01 220); - --divider: oklch(0.33 0.01 220); - --link: oklch(0.75 0.03 180); + @variant forest-light { + --color-background: oklch(0.98 0.02 140); + --color-foreground: oklch(0.15 0.08 130); + --color-card: oklch(0.98 0.02 140); + --color-card-foreground: oklch(0.15 0.08 130); + --color-popover: oklch(0.98 0.02 140); + --color-popover-foreground: oklch(0.15 0.08 130); + --color-primary: oklch(0.4 0.15 150); + --color-primary-foreground: oklch(0.98 0.02 140); + --color-secondary: oklch(0.92 0.06 140); + --color-secondary-foreground: oklch(0.15 0.08 130); + --color-muted: oklch(0.95 0.03 140); + --color-muted-foreground: oklch(0.5 0.08 130); + --color-accent: oklch(0.95 0.03 140); + --color-accent-foreground: oklch(0.15 0.08 130); + --color-destructive: oklch(0.577 0.245 27.325); + --color-border: oklch(0.88 0.04 140); + --color-input: oklch(0.88 0.04 140); + --color-ring: oklch(0.4 0.15 150); + --color-chart-1: oklch(0.5 0.18 150); + --color-chart-2: oklch(0.55 0.14 100); + --color-chart-3: oklch(0.6 0.12 80); + --color-chart-4: oklch(0.65 0.16 180); + --color-chart-5: oklch(0.58 0.15 130); + --color-sidebar: oklch(0.96 0.02 140); + --color-sidebar-foreground: oklch(0.15 0.08 130); + --color-sidebar-primary: oklch(0.4 0.15 150); + --color-sidebar-primary-foreground: oklch(0.98 0.02 140); + --color-sidebar-accent: oklch(0.95 0.03 140); + --color-sidebar-accent-foreground: oklch(0.15 0.08 130); + --color-sidebar-border: oklch(0.88 0.04 140); + --color-sidebar-ring: oklch(0.4 0.15 150); } - /* Tokyo Night (dark) */ - @variant tokyo-night { - --background: oklch(0.12 0.02 230); - --foreground: oklch(0.73 0.04 230); - --surface: oklch(0.15 0.02 230); - --surface-foreground: oklch(0.73 0.04 230); - --overlay: oklch(0.18 0.02 230); - --overlay-foreground: oklch(0.73 0.04 230); - --muted: oklch(0.4 0.03 230); - --default: oklch(0.15 0.02 230); - --default-foreground: oklch(0.73 0.04 230); - --accent: oklch(0.7 0.15 230); - --accent-foreground: oklch(0.12 0.02 230); - --success: oklch(0.82 0.15 100); - --success-foreground: oklch(0.12 0.02 230); - --warning: oklch(0.85 0.15 60); - --warning-foreground: oklch(0.12 0.02 230); - --danger: oklch(0.72 0.18 355); - --danger-foreground: oklch(0.12 0.02 230); - --field-background: oklch(0.15 0.02 230); - --field-foreground: oklch(0.73 0.04 230); - --field-placeholder: oklch(0.4 0.03 230); - --field-border: oklch(0.25 0.025 230); - --segment: oklch(0.25 0.025 230); - --segment-foreground: oklch(0.73 0.04 230); - --border: oklch(0.25 0.025 230); - --divider: oklch(0.25 0.025 230); - --link: oklch(0.7 0.15 230); + @variant forest-dark { + --color-background: oklch(0.1 0.05 130); + --color-foreground: oklch(0.96 0.01 140); + --color-card: oklch(0.18 0.06 130); + --color-card-foreground: oklch(0.96 0.01 140); + --color-popover: oklch(0.18 0.06 130); + --color-popover-foreground: oklch(0.96 0.01 140); + --color-primary: oklch(0.6 0.2 150); + --color-primary-foreground: oklch(0.1 0.05 130); + --color-secondary: oklch(0.25 0.08 130); + --color-secondary-foreground: oklch(0.96 0.01 140); + --color-muted: oklch(0.28 0.06 130); + --color-muted-foreground: oklch(0.65 0.05 140); + --color-accent: oklch(0.28 0.06 130); + --color-accent-foreground: oklch(0.96 0.01 140); + --color-destructive: oklch(0.704 0.191 22.216); + --color-border: oklch(0.32 0.08 130); + --color-input: oklch(0.32 0.08 130); + --color-ring: oklch(0.6 0.2 150); + --color-chart-1: oklch(0.65 0.2 150); + --color-chart-2: oklch(0.7 0.16 100); + --color-chart-3: oklch(0.75 0.14 80); + --color-chart-4: oklch(0.7 0.18 180); + --color-chart-5: oklch(0.68 0.17 130); + --color-sidebar: oklch(0.18 0.06 130); + --color-sidebar-foreground: oklch(0.96 0.01 140); + --color-sidebar-primary: oklch(0.6 0.2 150); + --color-sidebar-primary-foreground: oklch(0.1 0.05 130); + --color-sidebar-accent: oklch(0.28 0.06 130); + --color-sidebar-accent-foreground: oklch(0.96 0.01 140); + --color-sidebar-border: oklch(0.32 0.08 130); + --color-sidebar-ring: oklch(0.6 0.2 150); + } + @variant sunset-light { + --color-background: oklch(0.98 0.03 45); + --color-foreground: oklch(0.15 0.12 35); + --color-card: oklch(0.98 0.03 45); + --color-card-foreground: oklch(0.15 0.12 35); + --color-popover: oklch(0.98 0.03 45); + --color-popover-foreground: oklch(0.15 0.12 35); + --color-primary: oklch(0.5 0.2 40); + --color-primary-foreground: oklch(0.98 0.03 45); + --color-secondary: oklch(0.92 0.08 40); + --color-secondary-foreground: oklch(0.15 0.12 35); + --color-muted: oklch(0.95 0.04 45); + --color-muted-foreground: oklch(0.5 0.08 35); + --color-accent: oklch(0.95 0.04 45); + --color-accent-foreground: oklch(0.15 0.12 35); + --color-destructive: oklch(0.577 0.245 27.325); + --color-border: oklch(0.87 0.06 40); + --color-input: oklch(0.87 0.06 40); + --color-ring: oklch(0.5 0.2 40); + --color-chart-1: oklch(0.6 0.22 35); + --color-chart-2: oklch(0.65 0.18 25); + --color-chart-3: oklch(0.55 0.16 55); + --color-chart-4: oklch(0.7 0.2 30); + --color-chart-5: oklch(0.62 0.19 40); + --color-sidebar: oklch(0.96 0.03 45); + --color-sidebar-foreground: oklch(0.15 0.12 35); + --color-sidebar-primary: oklch(0.5 0.2 40); + --color-sidebar-primary-foreground: oklch(0.98 0.03 45); + --color-sidebar-accent: oklch(0.95 0.04 45); + --color-sidebar-accent-foreground: oklch(0.15 0.12 35); + --color-sidebar-border: oklch(0.87 0.06 40); + --color-sidebar-ring: oklch(0.5 0.2 40); + } + @variant sunset-dark { + --color-background: oklch(0.12 0.06 35); + --color-foreground: oklch(0.96 0.01 45); + --color-card: oklch(0.2 0.07 35); + --color-card-foreground: oklch(0.96 0.01 45); + --color-popover: oklch(0.2 0.07 35); + --color-popover-foreground: oklch(0.96 0.01 45); + --color-primary: oklch(0.7 0.22 40); + --color-primary-foreground: oklch(0.12 0.06 35); + --color-secondary: oklch(0.28 0.09 35); + --color-secondary-foreground: oklch(0.96 0.01 45); + --color-muted: oklch(0.32 0.07 35); + --color-muted-foreground: oklch(0.65 0.05 45); + --color-accent: oklch(0.32 0.07 35); + --color-accent-foreground: oklch(0.96 0.01 45); + --color-destructive: oklch(0.704 0.191 22.216); + --color-border: oklch(0.38 0.09 35); + --color-input: oklch(0.38 0.09 35); + --color-ring: oklch(0.7 0.22 40); + --color-chart-1: oklch(0.75 0.22 35); + --color-chart-2: oklch(0.8 0.18 25); + --color-chart-3: oklch(0.7 0.16 55); + --color-chart-4: oklch(0.78 0.2 30); + --color-chart-5: oklch(0.72 0.19 40); + --color-sidebar: oklch(0.2 0.07 35); + --color-sidebar-foreground: oklch(0.96 0.01 45); + --color-sidebar-primary: oklch(0.7 0.22 40); + --color-sidebar-primary-foreground: oklch(0.12 0.06 35); + --color-sidebar-accent: oklch(0.32 0.07 35); + --color-sidebar-accent-foreground: oklch(0.96 0.01 45); + --color-sidebar-border: oklch(0.38 0.09 35); + --color-sidebar-ring: oklch(0.7 0.22 40); + } + @variant lavender-light { + --color-background: oklch(0.98 0.02 300); + --color-foreground: oklch(0.2 0.12 290); + --color-card: oklch(0.98 0.02 300); + --color-card-foreground: oklch(0.2 0.12 290); + --color-popover: oklch(0.98 0.02 300); + --color-popover-foreground: oklch(0.2 0.12 290); + --color-primary: oklch(0.5 0.2 310); + --color-primary-foreground: oklch(0.98 0.02 300); + --color-secondary: oklch(0.92 0.08 300); + --color-secondary-foreground: oklch(0.2 0.12 290); + --color-muted: oklch(0.95 0.04 300); + --color-muted-foreground: oklch(0.55 0.08 290); + --color-accent: oklch(0.95 0.04 300); + --color-accent-foreground: oklch(0.2 0.12 290); + --color-destructive: oklch(0.577 0.245 27.325); + --color-border: oklch(0.87 0.06 300); + --color-input: oklch(0.87 0.06 300); + --color-ring: oklch(0.5 0.2 310); + --color-chart-1: oklch(0.6 0.22 310); + --color-chart-2: oklch(0.55 0.18 280); + --color-chart-3: oklch(0.65 0.16 340); + --color-chart-4: oklch(0.7 0.2 295); + --color-chart-5: oklch(0.58 0.19 305); + --color-sidebar: oklch(0.96 0.02 300); + --color-sidebar-foreground: oklch(0.2 0.12 290); + --color-sidebar-primary: oklch(0.5 0.2 310); + --color-sidebar-primary-foreground: oklch(0.98 0.02 300); + --color-sidebar-accent: oklch(0.95 0.04 300); + --color-sidebar-accent-foreground: oklch(0.2 0.12 290); + --color-sidebar-border: oklch(0.87 0.06 300); + --color-sidebar-ring: oklch(0.5 0.2 310); + } + @variant lavender-dark { + --color-background: oklch(0.13 0.06 290); + --color-foreground: oklch(0.96 0.01 300); + --color-card: oklch(0.2 0.07 290); + --color-card-foreground: oklch(0.96 0.01 300); + --color-popover: oklch(0.2 0.07 290); + --color-popover-foreground: oklch(0.96 0.01 300); + --color-primary: oklch(0.7 0.22 310); + --color-primary-foreground: oklch(0.13 0.06 290); + --color-secondary: oklch(0.28 0.09 290); + --color-secondary-foreground: oklch(0.96 0.01 300); + --color-muted: oklch(0.32 0.07 290); + --color-muted-foreground: oklch(0.65 0.05 300); + --color-accent: oklch(0.32 0.07 290); + --color-accent-foreground: oklch(0.96 0.01 300); + --color-destructive: oklch(0.704 0.191 22.216); + --color-border: oklch(0.38 0.09 290); + --color-input: oklch(0.38 0.09 290); + --color-ring: oklch(0.7 0.22 310); + --color-chart-1: oklch(0.75 0.22 310); + --color-chart-2: oklch(0.7 0.18 280); + --color-chart-3: oklch(0.78 0.16 340); + --color-chart-4: oklch(0.75 0.2 295); + --color-chart-5: oklch(0.72 0.19 305); + --color-sidebar: oklch(0.2 0.07 290); + --color-sidebar-foreground: oklch(0.96 0.01 300); + --color-sidebar-primary: oklch(0.7 0.22 310); + --color-sidebar-primary-foreground: oklch(0.13 0.06 290); + --color-sidebar-accent: oklch(0.32 0.07 290); + --color-sidebar-accent-foreground: oklch(0.96 0.01 300); + --color-sidebar-border: oklch(0.38 0.09 290); + --color-sidebar-ring: oklch(0.7 0.22 310); } } } diff --git a/styles/global.css b/styles/global.css index f4264cf..0a82d3e 100644 --- a/styles/global.css +++ b/styles/global.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "uniwind"; -@import "heroui-native/styles"; +@import "tw-animate-css"; + @import "./sources.css"; @import "./theme.css"; diff --git a/styles/sources.css b/styles/sources.css index 68a5b10..5425d66 100644 --- a/styles/sources.css +++ b/styles/sources.css @@ -1,3 +1,2 @@ -@source "../node_modules/heroui-native/lib"; @source "../app/**/*.{jsx,tsx}"; @source "../components/**/*.{jsx,tsx}"; diff --git a/styles/theme.css b/styles/theme.css index c983d71..cf4d175 100644 --- a/styles/theme.css +++ b/styles/theme.css @@ -5,25 +5,7 @@ --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; --font-mono: monospace; --radius: 0.5rem; - --shadow-x: 0px; - --shadow-y: 1px; - --shadow-blur: 3px; - --shadow-spread: 0px; - --shadow-opacity: 0.17; - --shadow-color: #000000; - --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09); - --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09); - --shadow-sm: - 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17); - --shadow: - 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17); - --shadow-md: - 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17); - --shadow-lg: - 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17); - --shadow-xl: - 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17); - --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43); --tracking-normal: 0.025em; --spacing: 0.25rem; + --spacing-hairline: hairlineWidth(); } diff --git a/tests/index.test.ts b/tests/index.test.ts index c8eed19..6aa5556 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,11 +1,11 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest'; -describe("Example Test Suite", () => { - it("should pass a basic test", () => { +describe('Example Test Suite', () => { + it('should pass a basic test', () => { expect(2 + 2).toBe(4); }); - it("should handle string concatenation", () => { - expect("hello" + " " + "world").toBe("hello world"); + it('should handle string concatenation', () => { + expect('hello' + ' ' + 'world').toBe('hello world'); }); }); diff --git a/tests/vitest-setup.ts b/tests/vitest-setup.ts index 1b7f564..6281dcc 100644 --- a/tests/vitest-setup.ts +++ b/tests/vitest-setup.ts @@ -1,8 +1,8 @@ -import "@testing-library/jest-dom"; -import { vi } from "vitest"; +import '@testing-library/jest-dom'; +import { vi } from 'vitest'; // NOTE: Mock window.matchMedia -Object.defineProperty(window, "matchMedia", { +Object.defineProperty(window, 'matchMedia', { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches: false, @@ -19,7 +19,7 @@ Object.defineProperty(window, "matchMedia", { // NOTE: Mock IntersectionObserver global.IntersectionObserver = class MockIntersectionObserver { root: Element | null = null; - rootMargin = ""; + rootMargin = ''; thresholds: ReadonlyArray = []; observe() {} diff --git a/types/uniwind-types.d.ts b/types/uniwind-types.d.ts index 2da1045..286da30 100644 --- a/types/uniwind-types.d.ts +++ b/types/uniwind-types.d.ts @@ -3,7 +3,7 @@ declare module 'uniwind' { export interface UniwindConfig { - themes: readonly ['light', 'dark', 'nord', 'dracula', 'tokyo-night', 'catppuccin-mocha'] + themes: readonly ['light', 'dark', 'ocean-light', 'ocean-dark', 'forest-light', 'forest-dark', 'sunset-light', 'sunset-dark', 'lavender-light', 'lavender-dark'] } } diff --git a/vitest.config.ts b/vitest.config.ts index 2613e6a..f468192 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,17 +1,17 @@ /// -import path from "node:path"; -import { defineConfig } from "vitest/config"; +import path from 'node:path'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - environment: "node", + environment: 'node', globals: true, - exclude: ["node_modules/**"], + exclude: ['node_modules/**'], }, resolve: { alias: { - "@": path.resolve(__dirname, "./"), + '@': path.resolve(__dirname, './'), }, }, });