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 (
+
+ );
+}
+
+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, './'),
},
},
});