Slice 1 polish: archive dormant code, merge work-os into primitives, add futures
- Archive dormant apps (daemon, desktop, native, tui) and superseded packages/server into repos/ for reference - Archive 21 dead web components (chat page shell, work-os cluster, strays) into repos/web/; keep reused message-rendering primitives in components/chat - Merge @code/work-os into @code/primitives; work.ts absorbed via ./work subpath and barrel export; update all four importers - Add docs/futures.md tracking audio/video (blocked at Flue + provider), web layout cleanup, and message rendering quality - Repoint dev:zopu script to @code/agents (the live Flue server) - Note repos/ reference-code convention in AGENTS.md
21
repos/native/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
node_modules/
|
||||
.expo/
|
||||
dist/
|
||||
npm-debug.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
|
||||
# UniWind generated types
|
||||
uniwind-types.d.ts
|
||||
|
||||
17
repos/native/app.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"expo": {
|
||||
"scheme": "code",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"orientation": "default",
|
||||
"web": {
|
||||
"bundler": "metro"
|
||||
},
|
||||
"name": "code",
|
||||
"slug": "code",
|
||||
"plugins": ["expo-font"],
|
||||
"experiments": {
|
||||
"typedRoutes": true,
|
||||
"reactCompiler": true
|
||||
}
|
||||
}
|
||||
}
|
||||
5
repos/native/app/(auth)/_layout.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function AuthLayout() {
|
||||
return <Stack screenOptions={{ headerShown: false }} />;
|
||||
}
|
||||
24
repos/native/app/(auth)/login.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NativeLoginForm } from "@code/auth/native";
|
||||
import { router } from "expo-router";
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
|
||||
|
||||
export default function LoginScreen() {
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
className="flex-1 bg-background"
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerClassName="flex-grow justify-center p-6"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View className="mx-auto w-full max-w-md">
|
||||
<NativeLoginForm
|
||||
onNavigateSignUp={() => router.push("/(auth)/signup")}
|
||||
onSuccess={() => router.replace("/")}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
24
repos/native/app/(auth)/signup.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NativeSignupForm } from "@code/auth/native";
|
||||
import { router } from "expo-router";
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
|
||||
|
||||
export default function SignupScreen() {
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
className="flex-1 bg-background"
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerClassName="flex-grow justify-center p-6"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View className="mx-auto w-full max-w-md py-6">
|
||||
<NativeSignupForm
|
||||
onNavigateSignIn={() => router.push("/(auth)/login")}
|
||||
onSuccess={() => router.replace("/")}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
42
repos/native/app/(drawer)/(tabs)/_layout.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
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 (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
headerStyle: {
|
||||
backgroundColor: themeColorBackground,
|
||||
},
|
||||
headerTintColor: themeColorForeground,
|
||||
headerTitleStyle: {
|
||||
color: themeColorForeground,
|
||||
fontWeight: "600",
|
||||
},
|
||||
tabBarStyle: {
|
||||
backgroundColor: themeColorBackground,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: "Home",
|
||||
tabBarIcon: ({ color, size }) => <Ionicons name="home" size={size} color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="two"
|
||||
options={{
|
||||
title: "Explore",
|
||||
tabBarIcon: ({ color, size }) => <Ionicons name="compass" size={size} color={color} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
16
repos/native/app/(drawer)/(tabs)/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Card } from "heroui-native";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Container className="p-6">
|
||||
<View className="flex-1 justify-center items-center">
|
||||
<Card variant="secondary" className="p-8 items-center">
|
||||
<Card.Title className="text-3xl mb-2">Tab One</Card.Title>
|
||||
</Card>
|
||||
</View>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
16
repos/native/app/(drawer)/(tabs)/two.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Card } from "heroui-native";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
|
||||
export default function TabTwo() {
|
||||
return (
|
||||
<Container className="p-6">
|
||||
<View className="flex-1 justify-center items-center">
|
||||
<Card variant="secondary" className="p-8 items-center">
|
||||
<Card.Title className="text-3xl mb-2">TabTwo</Card.Title>
|
||||
</Card>
|
||||
</View>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
88
repos/native/app/(drawer)/_layout.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
||||
import { Link } from "expo-router";
|
||||
import { Drawer } from "expo-router/drawer";
|
||||
import { useThemeColor } from "heroui-native";
|
||||
import React, { useCallback } from "react";
|
||||
import { Pressable, Text } from "react-native";
|
||||
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
function DrawerLayout() {
|
||||
const themeColorForeground = useThemeColor("foreground");
|
||||
const themeColorBackground = useThemeColor("background");
|
||||
|
||||
const renderThemeToggle = useCallback(() => <ThemeToggle />, []);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
screenOptions={{
|
||||
headerTintColor: themeColorForeground,
|
||||
headerStyle: { backgroundColor: themeColorBackground },
|
||||
headerTitleStyle: {
|
||||
fontWeight: "600",
|
||||
color: themeColorForeground,
|
||||
},
|
||||
headerRight: renderThemeToggle,
|
||||
drawerStyle: { backgroundColor: themeColorBackground },
|
||||
}}
|
||||
>
|
||||
<Drawer.Screen
|
||||
name="index"
|
||||
options={{
|
||||
headerTitle: "Home",
|
||||
drawerLabel: ({ color, focused }) => (
|
||||
<Text style={{ color: focused ? color : themeColorForeground }}>Home</Text>
|
||||
),
|
||||
drawerIcon: ({ size, color, focused }) => (
|
||||
<Ionicons
|
||||
name="home-outline"
|
||||
size={size}
|
||||
color={focused ? color : themeColorForeground}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="(tabs)"
|
||||
options={{
|
||||
headerTitle: "Tabs",
|
||||
drawerLabel: ({ color, focused }) => (
|
||||
<Text style={{ color: focused ? color : themeColorForeground }}>Tabs</Text>
|
||||
),
|
||||
drawerIcon: ({ size, color, focused }) => (
|
||||
<MaterialIcons
|
||||
name="border-bottom"
|
||||
size={size}
|
||||
color={focused ? color : themeColorForeground}
|
||||
/>
|
||||
),
|
||||
headerRight: () => (
|
||||
<Link href="/modal" asChild>
|
||||
<Pressable className="mr-4">
|
||||
<Ionicons name="add-outline" size={24} color={themeColorForeground} />
|
||||
</Pressable>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="todos"
|
||||
options={{
|
||||
headerTitle: "Todos",
|
||||
drawerLabel: ({ color, focused }) => (
|
||||
<Text style={{ color: focused ? color : themeColorForeground }}>Todos</Text>
|
||||
),
|
||||
drawerIcon: ({ size, color, focused }) => (
|
||||
<Ionicons
|
||||
name="checkbox-outline"
|
||||
size={size}
|
||||
color={focused ? color : themeColorForeground}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default DrawerLayout;
|
||||
53
repos/native/app/(drawer)/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { authClient } from "@code/auth/native";
|
||||
import { api } from "@code/backend/convex/_generated/api";
|
||||
import { useQuery } from "convex/react";
|
||||
import { Button, Spinner, Surface } from "heroui-native";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
|
||||
export default function Home() {
|
||||
const healthCheck = useQuery(api.healthCheck.get);
|
||||
const user = useQuery(api.auth.getCurrentUser);
|
||||
|
||||
return (
|
||||
<Container className="px-4 pb-4">
|
||||
<View className="mb-5 py-6">
|
||||
<Text className="text-3xl font-semibold tracking-tight text-foreground">Zopu</Text>
|
||||
<Text className="mt-1 text-sm text-muted">Your authenticated workspace</Text>
|
||||
</View>
|
||||
|
||||
<Surface className="mb-4 rounded-xl p-4" variant="secondary">
|
||||
{user === undefined ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
<View className="flex-row items-center justify-between gap-4">
|
||||
<View className="flex-1">
|
||||
<Text className="font-medium text-foreground">{user?.name ?? "Signed in"}</Text>
|
||||
<Text className="mt-0.5 text-xs text-muted">{user?.email}</Text>
|
||||
</View>
|
||||
<Button onPress={() => void authClient.signOut()} size="sm" variant="danger">
|
||||
<Button.Label>Sign out</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</Surface>
|
||||
|
||||
<Surface className="rounded-xl p-4" variant="secondary">
|
||||
<Text className="mb-2 font-medium text-foreground">API status</Text>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<View
|
||||
className={`h-2 w-2 rounded-full ${healthCheck === "OK" ? "bg-success" : "bg-danger"}`}
|
||||
/>
|
||||
<Text className="text-xs text-muted">
|
||||
{healthCheck === undefined
|
||||
? "Checking…"
|
||||
: healthCheck === "OK"
|
||||
? "Connected to Convex"
|
||||
: "Convex unavailable"}
|
||||
</Text>
|
||||
</View>
|
||||
</Surface>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
149
repos/native/app/(drawer)/todos.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { api } from "@code/backend/convex/_generated/api";
|
||||
import type { Doc, Id } from "@code/backend/convex/_generated/dataModel";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useMutation, useQuery } from "convex/react";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Chip,
|
||||
Spinner,
|
||||
Surface,
|
||||
Input,
|
||||
TextField,
|
||||
useThemeColor,
|
||||
} from "heroui-native";
|
||||
import { useState } from "react";
|
||||
import { View, Text, ScrollView, Alert } from "react-native";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
|
||||
export default function TodosScreen() {
|
||||
const [newTodoText, setNewTodoText] = useState("");
|
||||
const todos = useQuery(api.todos.getAll);
|
||||
const createTodoMutation = useMutation(api.todos.create);
|
||||
const toggleTodoMutation = useMutation(api.todos.toggle);
|
||||
const deleteTodoMutation = useMutation(api.todos.deleteTodo);
|
||||
|
||||
const mutedColor = useThemeColor("muted");
|
||||
const dangerColor = useThemeColor("danger");
|
||||
const foregroundColor = useThemeColor("foreground");
|
||||
|
||||
const handleAddTodo = async () => {
|
||||
const text = newTodoText.trim();
|
||||
if (!text) return;
|
||||
await createTodoMutation({ text });
|
||||
setNewTodoText("");
|
||||
};
|
||||
|
||||
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
|
||||
toggleTodoMutation({ id, completed: !currentCompleted });
|
||||
};
|
||||
|
||||
const handleDeleteTodo = (id: Id<"todos">) => {
|
||||
Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Delete",
|
||||
style: "destructive",
|
||||
onPress: () => deleteTodoMutation({ id }),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const isLoading = !todos;
|
||||
const completedCount = todos?.filter((t: Doc<"todos">) => t.completed).length || 0;
|
||||
const totalCount = todos?.length || 0;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ScrollView className="flex-1" contentContainerClassName="p-4">
|
||||
<View className="py-4 mb-4">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-2xl font-semibold text-foreground tracking-tight">Tasks</Text>
|
||||
{totalCount > 0 && (
|
||||
<Chip variant="secondary" color="accent" size="sm">
|
||||
<Chip.Label>
|
||||
{completedCount}/{totalCount}
|
||||
</Chip.Label>
|
||||
</Chip>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Surface variant="secondary" className="mb-4 p-3 rounded-lg">
|
||||
<View className="flex-row items-center gap-2">
|
||||
<View className="flex-1">
|
||||
<TextField>
|
||||
<Input
|
||||
value={newTodoText}
|
||||
onChangeText={setNewTodoText}
|
||||
placeholder="Add a new task..."
|
||||
onSubmitEditing={handleAddTodo}
|
||||
returnKeyType="done"
|
||||
/>
|
||||
</TextField>
|
||||
</View>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant={!newTodoText.trim() ? "secondary" : "primary"}
|
||||
isDisabled={!newTodoText.trim()}
|
||||
onPress={handleAddTodo}
|
||||
size="sm"
|
||||
>
|
||||
<Ionicons
|
||||
name="add"
|
||||
size={20}
|
||||
color={newTodoText.trim() ? foregroundColor : mutedColor}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
</Surface>
|
||||
|
||||
{isLoading && (
|
||||
<View className="items-center justify-center py-12">
|
||||
<Spinner size="lg" />
|
||||
<Text className="text-muted text-sm mt-3">Loading tasks...</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{todos && todos.length === 0 && !isLoading && (
|
||||
<Surface variant="secondary" className="items-center justify-center py-10 rounded-lg">
|
||||
<Ionicons name="checkbox-outline" size={40} color={mutedColor} />
|
||||
<Text className="text-foreground font-medium mt-3">No tasks yet</Text>
|
||||
<Text className="text-muted text-xs mt-1">Add your first task to get started</Text>
|
||||
</Surface>
|
||||
)}
|
||||
|
||||
{todos && todos.length > 0 && (
|
||||
<View className="gap-2">
|
||||
{todos.map((todo: Doc<"todos">) => (
|
||||
<Surface key={todo._id} variant="secondary" className="p-3 rounded-lg">
|
||||
<View className="flex-row items-center gap-3">
|
||||
<Checkbox
|
||||
isSelected={todo.completed}
|
||||
onSelectedChange={() => handleToggleTodo(todo._id, todo.completed)}
|
||||
/>
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
className={`text-sm ${todo.completed ? "text-muted line-through" : "text-foreground"}`}
|
||||
>
|
||||
{todo.text}
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="ghost"
|
||||
onPress={() => handleDeleteTodo(todo._id)}
|
||||
size="sm"
|
||||
>
|
||||
<Ionicons name="trash-outline" size={16} color={dangerColor} />
|
||||
</Button>
|
||||
</View>
|
||||
</Surface>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
27
repos/native/app/+not-found.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Link, Stack } from "expo-router";
|
||||
import { Button, Surface } from "heroui-native";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: "Not Found" }} />
|
||||
<Container>
|
||||
<View className="flex-1 justify-center items-center p-4">
|
||||
<Surface variant="secondary" className="items-center p-6 max-w-sm rounded-lg">
|
||||
<Text className="text-4xl mb-3">🤔</Text>
|
||||
<Text className="text-foreground font-medium text-lg mb-1">Page Not Found</Text>
|
||||
<Text className="text-muted text-sm text-center mb-4">
|
||||
The page you're looking for doesn't exist.
|
||||
</Text>
|
||||
<Link href="/" asChild>
|
||||
<Button size="sm">Go Home</Button>
|
||||
</Link>
|
||||
</Surface>
|
||||
</View>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
53
repos/native/app/_layout.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import "@/global.css";
|
||||
import { NativeAuthProvider } from "@code/auth/native";
|
||||
import { useConvexAuth } from "convex/react";
|
||||
import { Stack } from "expo-router";
|
||||
import { HeroUINativeProvider, Spinner } from "heroui-native";
|
||||
import { View } from "react-native";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { KeyboardProvider } from "react-native-keyboard-controller";
|
||||
|
||||
import { AppThemeProvider } from "@/contexts/app-theme-context";
|
||||
|
||||
function RootNavigator() {
|
||||
const { isAuthenticated, isLoading } = useConvexAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-background">
|
||||
<Spinner size="lg" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Protected guard={!isAuthenticated}>
|
||||
<Stack.Screen name="(auth)" />
|
||||
</Stack.Protected>
|
||||
<Stack.Protected guard={isAuthenticated}>
|
||||
<Stack.Screen name="(drawer)" />
|
||||
<Stack.Screen
|
||||
name="modal"
|
||||
options={{ headerShown: true, presentation: "modal", title: "Modal" }}
|
||||
/>
|
||||
</Stack.Protected>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<NativeAuthProvider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<KeyboardProvider>
|
||||
<AppThemeProvider>
|
||||
<HeroUINativeProvider>
|
||||
<RootNavigator />
|
||||
</HeroUINativeProvider>
|
||||
</AppThemeProvider>
|
||||
</KeyboardProvider>
|
||||
</GestureHandlerRootView>
|
||||
</NativeAuthProvider>
|
||||
);
|
||||
}
|
||||
37
repos/native/app/modal.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import { Button, Surface, useThemeColor } from "heroui-native";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
|
||||
function Modal() {
|
||||
const accentForegroundColor = useThemeColor("accent-foreground");
|
||||
|
||||
function handleClose() {
|
||||
router.back();
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<View className="flex-1 justify-center items-center p-4">
|
||||
<Surface variant="secondary" className="p-5 w-full max-w-sm rounded-lg">
|
||||
<View className="items-center">
|
||||
<View className="w-12 h-12 bg-accent rounded-lg items-center justify-center mb-3">
|
||||
<Ionicons name="checkmark" size={24} color={accentForegroundColor} />
|
||||
</View>
|
||||
<Text className="text-foreground font-medium text-lg mb-1">Modal Screen</Text>
|
||||
<Text className="text-muted text-sm text-center mb-4">
|
||||
This is an example modal screen for dialogs and confirmations.
|
||||
</Text>
|
||||
</View>
|
||||
<Button onPress={handleClose} className="w-full" size="sm">
|
||||
<Button.Label>Close</Button.Label>
|
||||
</Button>
|
||||
</Surface>
|
||||
</View>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default Modal;
|
||||
BIN
repos/native/assets/images/android-icon-background.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
repos/native/assets/images/android-icon-foreground.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
repos/native/assets/images/android-icon-monochrome.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
repos/native/assets/images/favicon.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
repos/native/assets/images/icon.png
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
repos/native/assets/images/partial-react-logo.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
repos/native/assets/images/react-logo.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
repos/native/assets/images/react-logo@2x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
repos/native/assets/images/react-logo@3x.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
repos/native/assets/images/splash-icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
46
repos/native/components/container.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { cn } from "heroui-native";
|
||||
import { type PropsWithChildren } from "react";
|
||||
import { ScrollView, View, type ScrollViewProps, type ViewProps } from "react-native";
|
||||
import Animated, { type AnimatedProps } from "react-native-reanimated";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const AnimatedView = Animated.createAnimatedComponent(View);
|
||||
|
||||
type Props = AnimatedProps<ViewProps> & {
|
||||
className?: string;
|
||||
isScrollable?: boolean;
|
||||
scrollViewProps?: Omit<ScrollViewProps, "contentContainerStyle">;
|
||||
};
|
||||
|
||||
export function Container({
|
||||
children,
|
||||
className,
|
||||
isScrollable = true,
|
||||
scrollViewProps,
|
||||
...props
|
||||
}: PropsWithChildren<Props>) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<AnimatedView
|
||||
className={cn("flex-1 bg-background", className)}
|
||||
style={{
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{isScrollable ? (
|
||||
<ScrollView
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
{...scrollViewProps}
|
||||
>
|
||||
{children}
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View className="flex-1">{children}</View>
|
||||
)}
|
||||
</AnimatedView>
|
||||
);
|
||||
}
|
||||
35
repos/native/components/theme-toggle.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { Platform, Pressable } from "react-native";
|
||||
import Animated, { FadeOut, ZoomIn } from "react-native-reanimated";
|
||||
import { withUniwind } from "uniwind";
|
||||
|
||||
import { useAppTheme } from "@/contexts/app-theme-context";
|
||||
|
||||
const StyledIonicons = withUniwind(Ionicons);
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { toggleTheme, isLight } = useAppTheme();
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (Platform.OS === "ios") {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
toggleTheme();
|
||||
}}
|
||||
className="px-2.5"
|
||||
>
|
||||
{isLight ? (
|
||||
<Animated.View key="moon" entering={ZoomIn} exiting={FadeOut}>
|
||||
<StyledIonicons name="moon" size={20} className="text-foreground" />
|
||||
</Animated.View>
|
||||
) : (
|
||||
<Animated.View key="sun" entering={ZoomIn} exiting={FadeOut}>
|
||||
<StyledIonicons name="sunny" size={20} className="text-foreground" />
|
||||
</Animated.View>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
55
repos/native/contexts/app-theme-context.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { createContext, useCallback, useContext, useMemo } from "react";
|
||||
import { Uniwind, useUniwind } from "uniwind";
|
||||
|
||||
type ThemeName = "light" | "dark";
|
||||
|
||||
type AppThemeContextType = {
|
||||
currentTheme: string;
|
||||
isLight: boolean;
|
||||
isDark: boolean;
|
||||
setTheme: (theme: ThemeName) => void;
|
||||
toggleTheme: () => void;
|
||||
};
|
||||
|
||||
const AppThemeContext = createContext<AppThemeContextType | undefined>(undefined);
|
||||
|
||||
export const AppThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const { theme } = useUniwind();
|
||||
|
||||
const isLight = useMemo(() => {
|
||||
return theme === "light";
|
||||
}, [theme]);
|
||||
|
||||
const isDark = useMemo(() => {
|
||||
return theme === "dark";
|
||||
}, [theme]);
|
||||
|
||||
const setTheme = useCallback((newTheme: ThemeName) => {
|
||||
Uniwind.setTheme(newTheme);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
Uniwind.setTheme(theme === "light" ? "dark" : "light");
|
||||
}, [theme]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
currentTheme: theme,
|
||||
isLight,
|
||||
isDark,
|
||||
setTheme,
|
||||
toggleTheme,
|
||||
}),
|
||||
[theme, isLight, isDark, setTheme, toggleTheme],
|
||||
);
|
||||
|
||||
return <AppThemeContext.Provider value={value}>{children}</AppThemeContext.Provider>;
|
||||
};
|
||||
|
||||
export function useAppTheme() {
|
||||
const context = useContext(AppThemeContext);
|
||||
if (!context) {
|
||||
throw new Error("useAppTheme must be used within AppThemeProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
5
repos/native/global.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@import "uniwind";
|
||||
@import "heroui-native/styles";
|
||||
|
||||
@source './node_modules/heroui-native/lib';
|
||||
13
repos/native/metro.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const { withUniwindConfig } = require("uniwind/metro");
|
||||
const { wrapWithReanimatedMetroConfig } = require("react-native-reanimated/metro-config");
|
||||
|
||||
/** @type {import('expo/metro-config').MetroConfig} */
|
||||
const config = getDefaultConfig(__dirname);
|
||||
|
||||
const uniwindConfig = withUniwindConfig(wrapWithReanimatedMetroConfig(config), {
|
||||
cssEntryFile: "./global.css",
|
||||
dtsFile: "./uniwind-types.d.ts",
|
||||
});
|
||||
|
||||
module.exports = uniwindConfig;
|
||||
52
repos/native/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "native",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
"start": "bun --env-file=../../.env expo start",
|
||||
"dev": "bun --env-file=../../.env expo start --clear",
|
||||
"android": "bun --env-file=../../.env expo run:android",
|
||||
"ios": "bun --env-file=../../.env expo run:ios",
|
||||
"prebuild": "bun --env-file=../../.env expo prebuild",
|
||||
"web": "bun --env-file=../../.env expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@code/auth": "workspace:*",
|
||||
"@code/backend": "workspace:*",
|
||||
"@expo/metro-runtime": "~57.0.2",
|
||||
"@expo/vector-icons": "^15.1.1",
|
||||
"@gorhom/bottom-sheet": "^5.2.14",
|
||||
"convex": "catalog:",
|
||||
"expo": "~57.0.1",
|
||||
"expo-font": "~57.0.0",
|
||||
"expo-haptics": "~57.0.0",
|
||||
"expo-linking": "~57.0.1",
|
||||
"expo-network": "~57.0.0",
|
||||
"expo-router": "~57.0.2",
|
||||
"expo-status-bar": "~57.0.0",
|
||||
"expo-web-browser": "~57.0.0",
|
||||
"heroui-native": "catalog:",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-native": "0.86.0",
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
"react-native-keyboard-controller": "1.21.9",
|
||||
"react-native-reanimated": "4.5.0",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "4.25.2",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.10.0",
|
||||
"tailwind-merge": "catalog:",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "catalog:",
|
||||
"uniwind": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@code/config": "workspace:*",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/react": "~19.2.17",
|
||||
"typescript": "~6.0.3"
|
||||
}
|
||||
}
|
||||
10
repos/native/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
|
||||
}
|
||||
3
repos/native/uniwind-env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/// <reference types="uniwind/types" />
|
||||
|
||||
declare module "*.css";
|
||||