diff options
47 files changed, 1991 insertions, 433 deletions
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index ca3da0cb..1e6128c7 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -3,21 +3,23 @@ import "expo-dev-client"; import { useEffect } from "react"; import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { KeyboardProvider } from "react-native-keyboard-controller"; import { useRouter } from "expo-router"; import { Stack } from "expo-router/stack"; import { ShareIntentProvider, useShareIntent } from "expo-share-intent"; import { StatusBar } from "expo-status-bar"; import { StyledStack } from "@/components/navigation/stack"; import { Providers } from "@/lib/providers"; -import useAppSettings from "@/lib/settings"; +import { useColorScheme, useInitialAndroidBarSync } from "@/lib/useColorScheme"; import { cn } from "@/lib/utils"; -import { useColorScheme } from "nativewind"; +import { NAV_THEME } from "@/theme"; +import { ThemeProvider as NavThemeProvider } from "@react-navigation/native"; export default function RootLayout() { + useInitialAndroidBarSync(); const router = useRouter(); const { hasShareIntent } = useShareIntent(); - const { colorScheme, setColorScheme } = useColorScheme(); - const { settings } = useAppSettings(); + const { colorScheme, isDarkColorScheme } = useColorScheme(); useEffect(() => { if (hasShareIntent) { @@ -27,52 +29,55 @@ export default function RootLayout() { } }, [hasShareIntent]); - useEffect(() => { - setColorScheme(settings.theme); - }, [settings.theme]); - return ( <> - <StyledStack - layout={(props) => { - return ( - <GestureHandlerRootView style={{ flex: 1 }}> - <ShareIntentProvider> - <Providers>{props.children}</Providers> - </ShareIntentProvider> - </GestureHandlerRootView> - ); - }} - contentClassName={cn( - "w-full flex-1 bg-gray-100 text-foreground dark:bg-background", - colorScheme == "dark" ? "dark" : "light", - )} - screenOptions={{ - headerTitle: "", - headerTransparent: true, - }} - > - <Stack.Screen name="index" /> - <Stack.Screen - name="signin" - options={{ - headerShown: true, - headerBackVisible: true, - headerBackTitle: "Back", - title: "", - }} - /> - <Stack.Screen name="sharing" /> - <Stack.Screen - name="test-connection" - options={{ - title: "Test Connection", - headerShown: true, - presentation: "modal", - }} - /> - </StyledStack> - <StatusBar style="auto" /> + <KeyboardProvider statusBarTranslucent navigationBarTranslucent> + <NavThemeProvider value={NAV_THEME[colorScheme]}> + <StyledStack + layout={(props) => { + return ( + <GestureHandlerRootView style={{ flex: 1 }}> + <ShareIntentProvider> + <Providers>{props.children}</Providers> + </ShareIntentProvider> + </GestureHandlerRootView> + ); + }} + contentClassName={cn( + "w-full flex-1 bg-gray-100 text-foreground dark:bg-background", + colorScheme == "dark" ? "dark" : "light", + )} + screenOptions={{ + headerTitle: "", + headerTransparent: true, + }} + > + <Stack.Screen name="index" /> + <Stack.Screen + name="signin" + options={{ + headerShown: true, + headerBackVisible: true, + headerBackTitle: "Back", + title: "", + }} + /> + <Stack.Screen name="sharing" /> + <Stack.Screen + name="test-connection" + options={{ + title: "Test Connection", + headerShown: true, + presentation: "modal", + }} + /> + </StyledStack> + </NavThemeProvider> + </KeyboardProvider> + <StatusBar + key={`root-status-bar-${isDarkColorScheme ? "light" : "dark"}`} + style={isDarkColorScheme ? "light" : "dark"} + /> </> ); } diff --git a/apps/mobile/app/dashboard/(tabs)/_layout.tsx b/apps/mobile/app/dashboard/(tabs)/_layout.tsx index f1d90ee4..7419c348 100644 --- a/apps/mobile/app/dashboard/(tabs)/_layout.tsx +++ b/apps/mobile/app/dashboard/(tabs)/_layout.tsx @@ -1,9 +1,11 @@ import React, { useLayoutEffect } from "react"; import { Tabs, useNavigation } from "expo-router"; import { StyledTabs } from "@/components/navigation/tabs"; +import { useColorScheme } from "@/lib/useColorScheme"; import { ClipboardList, Home, Settings } from "lucide-react-native"; export default function TabLayout() { + const { colors } = useColorScheme(); const navigation = useNavigation(); // Hide the header on the parent screen useLayoutEffect(() => { @@ -18,6 +20,7 @@ export default function TabLayout() { sceneClassName="bg-gray-100 dark:bg-background" screenOptions={{ headerShown: false, + tabBarActiveTintColor: colors.foreground, }} > <Tabs.Screen diff --git a/apps/mobile/app/dashboard/(tabs)/index.tsx b/apps/mobile/app/dashboard/(tabs)/index.tsx index f70474a9..0a51b817 100644 --- a/apps/mobile/app/dashboard/(tabs)/index.tsx +++ b/apps/mobile/app/dashboard/(tabs)/index.tsx @@ -1,4 +1,4 @@ -import { Platform, Pressable, Text, View } from "react-native"; +import { Platform, Pressable, View } from "react-native"; import * as Haptics from "expo-haptics"; import * as ImagePicker from "expo-image-picker"; import { router } from "expo-router"; @@ -6,6 +6,7 @@ import UpdatingBookmarkList from "@/components/bookmarks/UpdatingBookmarkList"; import { TailwindResolver } from "@/components/TailwindResolver"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import PageTitle from "@/components/ui/PageTitle"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import useAppSettings from "@/lib/settings"; import { useUploadAsset } from "@/lib/upload"; @@ -89,16 +90,16 @@ export default function Home() { /> </View> <Pressable - className="flex flex-row items-center gap-1 rounded-lg border border-input bg-background px-4 py-2.5" + className="flex flex-row items-center gap-1 rounded-lg border border-input bg-card px-4 py-1" onPress={() => router.push("/dashboard/search")} > <TailwindResolver - className="text-muted-foreground" + className="text-muted" comp={(styles) => ( <Search size={16} color={styles?.color?.toString()} /> )} /> - <Text className="text-muted-foreground">Search</Text> + <Text className="text-muted">Search</Text> </Pressable> </View> } diff --git a/apps/mobile/app/dashboard/(tabs)/lists.tsx b/apps/mobile/app/dashboard/(tabs)/lists.tsx index 218c1de4..a2301c36 100644 --- a/apps/mobile/app/dashboard/(tabs)/lists.tsx +++ b/apps/mobile/app/dashboard/(tabs)/lists.tsx @@ -1,15 +1,17 @@ import { useEffect, useState } from "react"; -import { FlatList, Pressable, Text, View } from "react-native"; +import { FlatList, Pressable, View } from "react-native"; import * as Haptics from "expo-haptics"; import { Link, router } from "expo-router"; import FullPageError from "@/components/FullPageError"; -import { TailwindResolver } from "@/components/TailwindResolver"; +import ChevronRight from "@/components/ui/ChevronRight"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import PageTitle from "@/components/ui/PageTitle"; +import { Text } from "@/components/ui/Text"; import { api } from "@/lib/trpc"; +import { useColorScheme } from "@/lib/useColorScheme"; import { condProps } from "@/lib/utils"; -import { ChevronRight, Plus } from "lucide-react-native"; +import { Plus } from "lucide-react-native"; import { useBookmarkLists } from "@karakeep/shared-react/hooks/lists"; import { ZBookmarkListTreeNode } from "@karakeep/shared/utils/listUtils"; @@ -65,6 +67,7 @@ function traverseTree( } export default function Lists() { + const { colors } = useColorScheme(); const [refreshing, setRefreshing] = useState(false); const { data: lists, isPending, error, refetch } = useBookmarkLists(); const [showChildrenOf, setShowChildrenOf] = useState<Record<string, boolean>>( @@ -130,7 +133,7 @@ export default function Lists() { }} renderItem={(l) => ( <View - className="mx-2 flex flex-row items-center rounded-xl border border-input bg-white px-4 py-2 dark:bg-accent" + className="mx-2 flex flex-row items-center rounded-xl border border-input bg-card px-4 py-2" style={condProps({ condition: l.item.level > 0, props: { marginLeft: l.item.level * 20 }, @@ -146,28 +149,23 @@ export default function Lists() { })); }} > - <TailwindResolver - className="text-foreground" - comp={(style) => ( - <ChevronRight - color={style?.color?.toString()} - style={{ - transform: [ - { rotate: l.item.collapsed ? "0deg" : "90deg" }, - ], - }} - /> - )} + <ChevronRight + color={colors.foreground} + style={{ + transform: [ + { rotate: l.item.collapsed ? "0deg" : "90deg" }, + ], + }} /> </Pressable> )} <Link asChild key={l.item.id} href={l.item.href} className="flex-1"> <Pressable className="flex flex-row justify-between"> - <Text className="text-lg text-accent-foreground"> + <Text> {l.item.logo} {l.item.name} </Text> - <ChevronRight color="rgb(0, 122, 255)" /> + <ChevronRight /> </Pressable> </Link> </View> diff --git a/apps/mobile/app/dashboard/(tabs)/settings.tsx b/apps/mobile/app/dashboard/(tabs)/settings.tsx index 7b3dab4f..6d76308d 100644 --- a/apps/mobile/app/dashboard/(tabs)/settings.tsx +++ b/apps/mobile/app/dashboard/(tabs)/settings.tsx @@ -1,16 +1,17 @@ import { useEffect } from "react"; -import { ActivityIndicator, Pressable, Text, View } from "react-native"; +import { ActivityIndicator, Pressable, View } from "react-native"; import { Slider } from "react-native-awesome-slider"; import { useSharedValue } from "react-native-reanimated"; import { Link } from "expo-router"; import { Button } from "@/components/ui/Button"; +import ChevronRight from "@/components/ui/ChevronRight"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Divider } from "@/components/ui/Divider"; import PageTitle from "@/components/ui/PageTitle"; +import { Text } from "@/components/ui/Text"; import { useSession } from "@/lib/session"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; -import { ChevronRight } from "lucide-react-native"; export default function Dashboard() { const { logout } = useSession(); @@ -38,56 +39,50 @@ export default function Dashboard() { <CustomSafeAreaView> <PageTitle title="Settings" /> <View className="flex h-full w-full items-center gap-3 px-4 py-2"> - <View className="flex w-full gap-3 rounded-lg bg-white px-4 py-2 dark:bg-accent"> - <Text className="text-lg text-accent-foreground"> - {isSettingsLoading ? "Loading ..." : settings.address} - </Text> + <View className="flex w-full gap-3 rounded-lg bg-card px-4 py-2"> + <Text>{isSettingsLoading ? "Loading ..." : settings.address}</Text> <Divider orientation="horizontal" /> - <Text className="text-lg text-accent-foreground"> - {isLoading ? "Loading ..." : data?.email} - </Text> + <Text>{isLoading ? "Loading ..." : data?.email}</Text> </View> <Text className="w-full p-1 text-2xl font-bold text-foreground"> App Settings </Text> - <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-white px-4 py-2 dark:bg-accent"> + <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2"> <Link asChild href="/dashboard/settings/theme" className="flex-1"> <Pressable className="flex flex-row justify-between"> - <Text className="text-lg text-accent-foreground">Theme</Text> + <Text>Theme</Text> <View className="flex flex-row items-center gap-2"> - <Text className="text-lg text-muted-foreground"> + <Text className="text-muted-foreground"> { { light: "Light", dark: "Dark", system: "System" }[ settings.theme ] } </Text> - <ChevronRight color="rgb(0, 122, 255)" /> + <ChevronRight /> </View> </Pressable> </Link> </View> - <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-white px-4 py-2 dark:bg-accent"> + <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2"> <Link asChild href="/dashboard/settings/bookmark-default-view" className="flex-1" > <Pressable className="flex flex-row justify-between"> - <Text className="text-lg text-accent-foreground"> - Default Bookmark View - </Text> + <Text>Default Bookmark View</Text> <View className="flex flex-row items-center gap-2"> {isSettingsLoading ? ( <ActivityIndicator size="small" /> ) : ( - <Text className="text-lg text-muted-foreground"> + <Text className="text-muted-foreground"> {settings.defaultBookmarkView === "reader" ? "Reader" : "Browser"} </Text> )} - <ChevronRight color="rgb(0, 122, 255)" /> + <ChevronRight /> </View> </Pressable> </Link> @@ -95,8 +90,8 @@ export default function Dashboard() { <Text className="w-full p-1 text-2xl font-bold text-foreground"> Upload Settings </Text> - <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-white px-4 py-2 dark:bg-accent"> - <Text className="text-lg text-accent-foreground">Image Quality</Text> + <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2"> + <Text>Image Quality</Text> <View className="flex flex-1 flex-row items-center justify-center gap-2"> <Text className="text-foreground"> {Math.round(settings.imageQuality * 100)}% @@ -115,7 +110,13 @@ export default function Dashboard() { </View> </View> <Divider orientation="horizontal" /> - <Button className="w-full" label="Log Out" onPress={logout} /> + <Button + androidRootClassName="w-full" + onPress={logout} + variant="destructive" + > + <Text>Log Out</Text> + </Button> </View> </CustomSafeAreaView> ); diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx index eafcfc19..3b1300ca 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx @@ -25,6 +25,7 @@ import { Button } from "@/components/ui/Button"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { useAssetUrl } from "@/lib/hooks"; import useAppSettings from "@/lib/settings"; @@ -296,15 +297,17 @@ function BookmarkTextView({ bookmark }: { bookmark: ZBookmark }) { <View className="flex-1"> {isEditing && ( <View className="absolute right-0 top-0 z-10 m-4 flex flex-row gap-1"> - <Button label="Save" variant="default" onPress={Keyboard.dismiss} /> + <Button onPress={Keyboard.dismiss}> + <Text>Save</Text> + </Button> <Button - label="Discard" - variant="destructive" onPress={() => { setContent(initialText); setIsEditing(false); }} - /> + > + <Text>Discard</Text> + </Button> </View> )} <ScrollView className="flex bg-background p-2"> diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx index af124160..1781ec74 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx @@ -1,26 +1,22 @@ import React from "react"; +import { Alert, Pressable, View } from "react-native"; import { - Alert, - Keyboard, - Pressable, - Text, - TouchableWithoutFeedback, - View, -} from "react-native"; -import Animated, { - useAnimatedKeyboard, - useAnimatedStyle, -} from "react-native-reanimated"; + KeyboardAwareScrollView, + KeyboardGestureArea, +} from "react-native-keyboard-controller"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { router, Stack, useLocalSearchParams } from "expo-router"; import TagPill from "@/components/bookmarks/TagPill"; import FullPageError from "@/components/FullPageError"; import { Button } from "@/components/ui/Button"; +import ChevronRight from "@/components/ui/ChevronRight"; import { Divider } from "@/components/ui/Divider"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { Input } from "@/components/ui/Input"; import { Skeleton } from "@/components/ui/Skeleton"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; -import { ChevronRight } from "lucide-react-native"; +import { cn } from "@/lib/utils"; import { useAutoRefreshingBookmarkQuery, @@ -30,9 +26,21 @@ import { import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; import { isBookmarkStillTagging } from "@karakeep/shared/utils/bookmarkUtils"; +function InfoSection({ + className, + ...props +}: React.ComponentProps<typeof View>) { + return ( + <View + className={cn("flex gap-2 rounded-lg bg-card p-3", className)} + {...props} + /> + ); +} + function TagList({ bookmark }: { bookmark: ZBookmark }) { return ( - <View className="flex gap-2 rounded-lg bg-white py-3 dark:bg-accent"> + <InfoSection> {isBookmarkStillTagging(bookmark) ? ( <View className="flex gap-4 pb-3"> <Skeleton className="h-4 w-full" /> @@ -41,7 +49,7 @@ function TagList({ bookmark }: { bookmark: ZBookmark }) { ) : ( bookmark.tags.length > 0 && ( <> - <View className="flex flex-row flex-wrap gap-2 rounded-lg bg-background p-2"> + <View className="flex flex-row flex-wrap gap-2 rounded-lg p-2"> {bookmark.tags.map((t) => ( <TagPill key={t.id} tag={t} /> ))} @@ -50,94 +58,104 @@ function TagList({ bookmark }: { bookmark: ZBookmark }) { </> ) )} - <Pressable - onPress={() => - router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`) - } - className="flex w-full flex-row justify-between gap-3 px-4" - > - <Text className="text-lg text-accent-foreground">Manage Tags</Text> - <ChevronRight color="rgb(0, 122, 255)" /> - </Pressable> - </View> + <View> + <Pressable + onPress={() => + router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`) + } + className="flex w-full flex-row justify-between gap-3" + > + <Text>Manage Tags</Text> + <ChevronRight /> + </Pressable> + </View> + </InfoSection> ); } function ManageLists({ bookmark }: { bookmark: ZBookmark }) { return ( - <View className="flex gap-4"> - <Pressable - onPress={() => - router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`) - } - className="flex w-full flex-row justify-between gap-3 rounded-lg bg-white px-4 py-2 dark:bg-accent" - > - <Text className="text-lg text-accent-foreground">Manage Lists</Text> - <ChevronRight color="rgb(0, 122, 255)" /> - </Pressable> - </View> + <InfoSection> + <View> + <Pressable + onPress={() => + router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`) + } + className="flex w-full flex-row justify-between gap-3 rounded-lg" + > + <Text>Manage Lists</Text> + <ChevronRight /> + </Pressable> + </View> + </InfoSection> ); } function TitleEditor({ - bookmarkId, title, + setTitle, + isPending, }: { - bookmarkId: string; - title: string; + title: string | null | undefined; + setTitle: (title: string | null) => void; + isPending: boolean; }) { - const { mutate, isPending } = useUpdateBookmark(); return ( - <View className="flex gap-4"> + <InfoSection> <Input editable={!isPending} - multiline={true} + multiline={false} numberOfLines={1} - loading={isPending} placeholder="Title" - textAlignVertical="top" - onEndEditing={(ev) => - mutate({ - bookmarkId, - title: ev.nativeEvent.text ? ev.nativeEvent.text : null, - }) - } + onChangeText={(text) => setTitle(text)} defaultValue={title ?? ""} /> - </View> + </InfoSection> ); } -function NotesEditor({ bookmark }: { bookmark: ZBookmark }) { - const { mutate, isPending } = useUpdateBookmark(); +function NotesEditor({ + notes, + setNotes, + isPending, +}: { + notes: string | null | undefined; + setNotes: (title: string | null) => void; + isPending: boolean; +}) { return ( - <View className="flex gap-4"> + <InfoSection> <Input editable={!isPending} multiline={true} - numberOfLines={3} - loading={isPending} placeholder="Notes" + inputClasses="h-24" + onChangeText={(text) => setNotes(text)} textAlignVertical="top" - onEndEditing={(ev) => - mutate({ - bookmarkId: bookmark.id, - note: ev.nativeEvent.text, - }) - } - defaultValue={bookmark.note ?? ""} + defaultValue={notes ?? ""} /> - </View> + </InfoSection> ); } const ViewBookmarkPage = () => { + const insets = useSafeAreaInsets(); const { slug } = useLocalSearchParams(); const { toast } = useToast(); if (typeof slug !== "string") { throw new Error("Unexpected param type"); } + const { mutate: editBookmark, isPending: isEditPending } = useUpdateBookmark({ + onSuccess: () => { + toast({ + message: "The bookmark has been updated!", + showProgress: false, + }); + setEditedBookmark({}); + }, + }); + const { mutate: deleteBookmark, isPending: isDeletionPending } = useDeleteBookmark({ onSuccess: () => { @@ -149,12 +167,6 @@ const ViewBookmarkPage = () => { }, }); - const keyboard = useAnimatedKeyboard(); - - const animatedStyles = useAnimatedStyle(() => ({ - marginBottom: keyboard.height.value, - })); - const { data: bookmark, isPending, @@ -163,6 +175,11 @@ const ViewBookmarkPage = () => { bookmarkId: slug, }); + const [editedBookmark, setEditedBookmark] = React.useState<{ + title?: string | null; + note?: string; + }>({}); + if (isPending) { return <FullPageSpinner />; } @@ -188,6 +205,27 @@ const ViewBookmarkPage = () => { ); }; + const onDone = () => { + const doDone = () => { + if (router.canGoBack()) { + router.back(); + } else { + router.replace("dashboard"); + } + }; + if (Object.keys(editedBookmark).length === 0) { + doDone(); + return; + } + Alert.alert("You have unsaved changes", "Do you still want to leave?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Leave", + onPress: doDone, + }, + ]); + }; + let title = null; switch (bookmark.content.type) { case BookmarkTypes.LINK: @@ -201,56 +239,77 @@ const ViewBookmarkPage = () => { break; } return ( - <View> - <Stack.Screen - options={{ - headerShown: true, - headerTransparent: false, - headerTitle: title ?? "Untitled", - headerRight: () => ( - <Pressable - onPress={() => { - if (router.canGoBack()) { - router.back(); - } else { - router.replace("dashboard"); - } - }} + <KeyboardGestureArea interpolator="ios"> + <KeyboardAwareScrollView + className="p-4" + bottomOffset={8} + keyboardDismissMode="interactive" + contentContainerStyle={{ paddingBottom: insets.bottom }} + > + <Stack.Screen + options={{ + headerShown: true, + headerTransparent: false, + headerTitle: title ?? "Untitled", + headerRight: () => ( + <Pressable onPress={onDone}> + <Text>Done</Text> + </Pressable> + ), + }} + /> + <View className="gap-4"> + <TitleEditor + title={title} + setTitle={(title) => + setEditedBookmark((prev) => ({ ...prev, title })) + } + isPending={isEditPending} + /> + <TagList bookmark={bookmark} /> + <ManageLists bookmark={bookmark} /> + <NotesEditor + notes={bookmark.note} + setNotes={(note) => + setEditedBookmark((prev) => ({ ...prev, note: note ?? "" })) + } + isPending={isEditPending} + /> + <View className="flex justify-between gap-3"> + <Button + onPress={() => + editBookmark({ + bookmarkId: bookmark.id, + ...editedBookmark, + }) + } + disabled={isEditPending} > - <Text className="text-foreground">Done</Text> - </Pressable> - ), - }} - /> - <Animated.ScrollView className="p-4" style={[animatedStyles]}> - <TouchableWithoutFeedback onPress={Keyboard.dismiss}> - <View className="h-screen gap-8 px-2"> - <TitleEditor bookmarkId={bookmark.id} title={title ?? ""} /> - <TagList bookmark={bookmark} /> - <ManageLists bookmark={bookmark} /> - <NotesEditor bookmark={bookmark} /> + <Text>Save</Text> + </Button> <Button - onPress={handleDeleteBookmark} variant="destructive" + onPress={handleDeleteBookmark} disabled={isDeletionPending} - label="Delete" - /> - <View className="gap-2"> - <Text className="items-center text-center"> - Created {bookmark.createdAt.toLocaleString()} - </Text> - {bookmark.modifiedAt && - bookmark.modifiedAt.getTime() !== - bookmark.createdAt.getTime() && ( - <Text className="items-center text-center"> - Modified {bookmark.modifiedAt.toLocaleString()} - </Text> - )} - </View> + > + <Text>Delete</Text> + </Button> + </View> + <View className="gap-2"> + <Text className="items-center text-center"> + Created {bookmark.createdAt.toLocaleString()} + </Text> + {bookmark.modifiedAt && + bookmark.modifiedAt.getTime() !== + bookmark.createdAt.getTime() && ( + <Text className="items-center text-center"> + Modified {bookmark.modifiedAt.toLocaleString()} + </Text> + )} </View> - </TouchableWithoutFeedback> - </Animated.ScrollView> - </View> + </View> + </KeyboardAwareScrollView> + </KeyboardGestureArea> ); }; diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx index 9f2149ae..7250d06b 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx @@ -1,8 +1,9 @@ import React from "react"; -import { FlatList, Pressable, Text, View } from "react-native"; +import { FlatList, Pressable, View } from "react-native"; import Checkbox from "expo-checkbox"; import { useLocalSearchParams } from "expo-router"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { @@ -75,13 +76,13 @@ const ListPickerPage = () => { gap: 5, }} renderItem={(l) => ( - <View className="mx-2 flex flex-row items-center rounded-xl border border-input bg-white px-4 py-2 dark:bg-accent"> + <View className="mx-2 flex flex-row items-center rounded-xl border border-input bg-card px-4 py-2"> <Pressable key={l.item[l.item.length - 1].id} onPress={() => toggleList(l.item[l.item.length - 1].id)} className="flex w-full flex-row justify-between" > - <Text className="text-lg text-accent-foreground"> + <Text> {l.item.map((item) => `${item.icon} ${item.name}`).join(" / ")} </Text> <Checkbox diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx index 38296626..ea6c2f4d 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx @@ -1,16 +1,11 @@ import React, { useMemo } from "react"; -import { - Pressable, - SectionList, - Text, - TouchableOpacity, - View, -} from "react-native"; +import { Pressable, SectionList, TouchableOpacity, View } from "react-native"; import { Stack, useLocalSearchParams } from "expo-router"; -import { TailwindResolver } from "@/components/TailwindResolver"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; +import { useColorScheme } from "@/lib/useColorScheme"; import { Check, Plus } from "lucide-react-native"; import { @@ -22,6 +17,7 @@ import { api } from "@karakeep/shared-react/trpc"; const NEW_TAG_ID = "new-tag"; const ListPickerPage = () => { + const { colors } = useColorScheme(); const { slug: bookmarkId } = useLocalSearchParams(); const [search, setSearch] = React.useState(""); @@ -211,20 +207,14 @@ const ListPickerPage = () => { }) } > - <View className="mx-2 flex flex-row items-center gap-2 rounded-xl border border-input bg-white px-4 py-2 dark:bg-accent"> + <View className="mx-2 flex flex-row items-center gap-2 rounded-xl border border-input bg-card px-4 py-2"> {t.section.title == "Existing Tags" && ( - <TailwindResolver - className="text-accent-foreground" - comp={(s) => <Check color={s?.color} />} - /> + <Check color={colors.foreground} /> )} {t.section.title == "All Tags" && t.item.id == NEW_TAG_ID && ( - <TailwindResolver - className="text-accent-foreground" - comp={(s) => <Plus color={s?.color} />} - /> + <Plus color={colors.foreground} /> )} - <Text className="text-center text-lg text-accent-foreground"> + <Text> {t.item.id == NEW_TAG_ID ? `Create new tag '${t.item.name}'` : t.item.name} diff --git a/apps/mobile/app/dashboard/bookmarks/new.tsx b/apps/mobile/app/dashboard/bookmarks/new.tsx index d24c1597..50f8f2a7 100644 --- a/apps/mobile/app/dashboard/bookmarks/new.tsx +++ b/apps/mobile/app/dashboard/bookmarks/new.tsx @@ -1,9 +1,10 @@ import React, { useState } from "react"; -import { Text, View } from "react-native"; +import { View } from "react-native"; import { router } from "expo-router"; import { Button } from "@/components/ui/Button"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { useCreateBookmark } from "@karakeep/shared-react/hooks/bookmarks"; @@ -61,13 +62,16 @@ const NoteEditorPage = () => { )} <Input onChangeText={setText} + className="bg-card" multiline placeholder="What's on your mind?" autoFocus autoCapitalize={"none"} textAlignVertical="top" /> - <Button onPress={onSubmit} label="Save" /> + <Button onPress={onSubmit}> + <Text>Save</Text> + </Button> </View> </CustomSafeAreaView> ); diff --git a/apps/mobile/app/dashboard/lists/new.tsx b/apps/mobile/app/dashboard/lists/new.tsx index 2cd690f5..55315e70 100644 --- a/apps/mobile/app/dashboard/lists/new.tsx +++ b/apps/mobile/app/dashboard/lists/new.tsx @@ -1,9 +1,10 @@ import React, { useState } from "react"; -import { Text, View } from "react-native"; +import { View } from "react-native"; import { router } from "expo-router"; import { Button } from "@/components/ui/Button"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { useCreateBookmarkList } from "@karakeep/shared-react/hooks/lists"; @@ -40,14 +41,16 @@ const NewListPage = () => { <View className="flex flex-row items-center gap-1"> <Text className="shrink p-2">🚀</Text> <Input - className="flex-1" + className="flex-1 bg-card" onChangeText={setText} placeholder="List Name" autoFocus autoCapitalize={"none"} /> </View> - <Button disabled={isPending} onPress={onSubmit} label="Save" /> + <Button disabled={isPending} onPress={onSubmit}> + <Text>Save</Text> + </Button> </View> </CustomSafeAreaView> ); diff --git a/apps/mobile/app/dashboard/search.tsx b/apps/mobile/app/dashboard/search.tsx index 5cc97575..66423870 100644 --- a/apps/mobile/app/dashboard/search.tsx +++ b/apps/mobile/app/dashboard/search.tsx @@ -1,18 +1,12 @@ import { useMemo, useRef, useState } from "react"; -import { - FlatList, - Keyboard, - Pressable, - Text, - TextInput, - View, -} from "react-native"; -import { router } from "expo-router"; +import { FlatList, Keyboard, Pressable, TextInput, View } from "react-native"; +import { router, Stack } from "expo-router"; import BookmarkList from "@/components/bookmarks/BookmarkList"; import FullPageError from "@/components/FullPageError"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; -import { Input } from "@/components/ui/Input"; +import { SearchInput } from "@/components/ui/SearchInput"; +import { Text } from "@/components/ui/Text"; import { api } from "@/lib/trpc"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { keepPreviousData } from "@tanstack/react-query"; @@ -102,24 +96,26 @@ export default function Search() { return ( <CustomSafeAreaView> - <View className="flex flex-row items-center gap-3 p-3"> - <Input - ref={inputRef} - placeholder="Search" - className="flex-1" - value={search} - onChangeText={setSearch} - onFocus={handleOnFocus} - onBlur={handleOnBlur} - onSubmitEditing={() => handleSearchSubmit(search)} - returnKeyType="search" - autoFocus - autoCapitalize="none" - /> - <Pressable onPress={() => router.back()}> - <Text className="text-foreground">Cancel</Text> - </Pressable> - </View> + <Stack.Screen + options={{ + headerShown: true, + }} + /> + <SearchInput + containerClassName="m-3" + ref={inputRef} + placeholder="Search" + className="flex-1" + value={search} + onChangeText={setSearch} + onFocus={handleOnFocus} + onBlur={handleOnBlur} + onSubmitEditing={() => handleSearchSubmit(search)} + returnKeyType="search" + autoFocus + autoCapitalize="none" + onCancel={router.back} + /> {isInputFocused ? ( <FlatList diff --git a/apps/mobile/app/dashboard/settings/bookmark-default-view.tsx b/apps/mobile/app/dashboard/settings/bookmark-default-view.tsx index c8c522cf..5f4463ae 100644 --- a/apps/mobile/app/dashboard/settings/bookmark-default-view.tsx +++ b/apps/mobile/app/dashboard/settings/bookmark-default-view.tsx @@ -1,7 +1,8 @@ -import { Pressable, Text, View } from "react-native"; +import { Pressable, View } from "react-native"; import { useRouter } from "expo-router"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Divider } from "@/components/ui/Divider"; +import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import useAppSettings from "@/lib/settings"; import { Check } from "lucide-react-native"; @@ -41,9 +42,7 @@ export default function BookmarkDefaultViewSettings() { className="flex flex-row justify-between" key={mode} > - <Text className="text-lg text-accent-foreground"> - {{ browser: "Browser", reader: "Reader" }[mode]} - </Text> + <Text>{{ browser: "Browser", reader: "Reader" }[mode]}</Text> {isChecked && <Check color="rgb(0, 122, 255)" />} </Pressable>, <Divider @@ -59,7 +58,7 @@ export default function BookmarkDefaultViewSettings() { return ( <CustomSafeAreaView> <View className="flex h-full w-full items-center px-4 py-2"> - <View className="w-full rounded-lg bg-white px-4 py-2 dark:bg-accent"> + <View className="w-full rounded-lg bg-card bg-card px-4 py-2"> {options} </View> </View> diff --git a/apps/mobile/app/dashboard/settings/theme.tsx b/apps/mobile/app/dashboard/settings/theme.tsx index f7feacdb..a4f0494a 100644 --- a/apps/mobile/app/dashboard/settings/theme.tsx +++ b/apps/mobile/app/dashboard/settings/theme.tsx @@ -1,6 +1,7 @@ -import { Pressable, Text, View } from "react-native"; +import { Pressable, View } from "react-native"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Divider } from "@/components/ui/Divider"; +import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { Check } from "lucide-react-native"; @@ -16,7 +17,7 @@ export default function ThemePage() { className="flex flex-row justify-between" key={theme} > - <Text className="text-lg text-accent-foreground"> + <Text> { { light: "Light Mode", dark: "Dark Mode", system: "System" }[ theme @@ -38,9 +39,7 @@ export default function ThemePage() { return ( <CustomSafeAreaView> <View className="flex h-full w-full items-center px-4 py-2"> - <View className="w-full rounded-lg bg-white px-4 py-2 dark:bg-accent"> - {options} - </View> + <View className="w-full rounded-lg bg-card px-4 py-2">{options}</View> </View> </CustomSafeAreaView> ); diff --git a/apps/mobile/app/error.tsx b/apps/mobile/app/error.tsx index d0e4a7df..6e975306 100644 --- a/apps/mobile/app/error.tsx +++ b/apps/mobile/app/error.tsx @@ -1,9 +1,10 @@ -import { Text, View } from "react-native"; +import { View } from "react-native"; +import { Text } from "@/components/ui/Text"; export default function ErrorPage() { return ( <View className="flex-1 items-center justify-center gap-4"> - <Text className="text-4xl">Error!</Text> + <Text variant="largeTitle">Error!</Text> </View> ); } diff --git a/apps/mobile/app/sharing.tsx b/apps/mobile/app/sharing.tsx index 506b5100..1e5df4b8 100644 --- a/apps/mobile/app/sharing.tsx +++ b/apps/mobile/app/sharing.tsx @@ -1,8 +1,9 @@ import { useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Pressable, Text, View } from "react-native"; +import { ActivityIndicator, Pressable, View } from "react-native"; import { useRouter } from "expo-router"; import { useShareIntentContext } from "expo-share-intent"; import { Button } from "@/components/ui/Button"; +import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; import { useUploadAsset } from "@/lib/upload"; @@ -73,7 +74,7 @@ function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) { return ( <View className="flex flex-row gap-3"> - <Text className="text-4xl text-foreground">Hoarding</Text> + <Text variant="largeTitle">Hoarding</Text> <ActivityIndicator /> </View> ); @@ -95,18 +96,19 @@ export default function Sharing() { case "success": { comp = ( <View className="items-center gap-4"> - <Text className="text-4xl text-foreground"> + <Text variant="largeTitle"> {mode.type === "alreadyExists" ? "Already Hoarded!" : "Hoarded!"} </Text> <Button - label="Manage" onPress={() => { router.replace(`/dashboard/bookmarks/${mode.bookmarkId}/info`); if (autoCloseTimeoutId.current) { clearTimeout(autoCloseTimeoutId.current); } }} - /> + > + <Text>Manage</Text> + </Button> <Pressable onPress={() => router.replace("dashboard")}> <Text className="text-muted-foreground">Dismiss</Text> </Pressable> @@ -115,7 +117,7 @@ export default function Sharing() { break; } case "error": { - comp = <Text className="text-4xl text-foreground">Error!</Text>; + comp = <Text variant="largeTitle">Error!</Text>; break; } } diff --git a/apps/mobile/app/signin.tsx b/apps/mobile/app/signin.tsx index 0d160398..215b6a67 100644 --- a/apps/mobile/app/signin.tsx +++ b/apps/mobile/app/signin.tsx @@ -4,18 +4,17 @@ import { KeyboardAvoidingView, Platform, Pressable, - Text, TouchableWithoutFeedback, View, } from "react-native"; import { Redirect, useRouter } from "expo-router"; import Logo from "@/components/Logo"; import { TailwindResolver } from "@/components/TailwindResolver"; -import { Button, buttonVariants } from "@/components/ui/Button"; +import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; -import { cn } from "@/lib/utils"; import { Bug } from "lucide-react-native"; enum LoginType { @@ -134,6 +133,7 @@ export default function Signin() { <Text className="font-bold">Server Address</Text> <Input className="w-full" + inputClasses="bg-card" placeholder="Server Address" value={formState.serverAddress} autoCapitalize="none" @@ -150,6 +150,7 @@ export default function Signin() { <Text className="font-bold">Email</Text> <Input className="w-full" + inputClasses="bg-card" placeholder="Email" keyboardType="email-address" autoCapitalize="none" @@ -163,6 +164,7 @@ export default function Signin() { <Text className="font-bold">Password</Text> <Input className="w-full" + inputClasses="bg-card" placeholder="Password" secureTextEntry value={formState.password} @@ -181,6 +183,7 @@ export default function Signin() { <Text className="font-bold">API Key</Text> <Input className="w-full" + inputClasses="bg-card" placeholder="API Key" secureTextEntry value={formState.apiKey} @@ -193,18 +196,17 @@ export default function Signin() { <View className="flex flex-row items-center justify-between gap-2"> <Button - className="flex-1" - label="Sign In" + size="lg" + androidRootClassName="flex-1" onPress={onSignin} disabled={ userNamePasswordRequestIsPending || apiKeyValueRequestIsPending } - /> - <Pressable - className={cn( - buttonVariants({ variant: "default" }), - !settings.address && "bg-gray-500", - )} + > + <Text>Sign In</Text> + </Button> + <Button + size="icon" onPress={() => router.push("/test-connection")} disabled={!settings.address} > @@ -212,9 +214,9 @@ export default function Signin() { comp={(styles) => ( <Bug size={20} color={styles?.color?.toString()} /> )} - className="text-background" + className="text-white" /> - </Pressable> + </Button> </View> <Pressable onPress={toggleLoginType}> <Text className="mt-2 text-center text-gray-500"> diff --git a/apps/mobile/app/test-connection.tsx b/apps/mobile/app/test-connection.tsx index 5639c6bd..a9ec6e5e 100644 --- a/apps/mobile/app/test-connection.tsx +++ b/apps/mobile/app/test-connection.tsx @@ -1,9 +1,10 @@ import React from "react"; -import { Platform, Text, View } from "react-native"; +import { Platform, View } from "react-native"; import * as Clipboard from "expo-clipboard"; import { Button } from "@/components/ui/Button"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { cn } from "@/lib/utils"; import { z } from "zod"; @@ -79,19 +80,22 @@ export default function TestConnection() { <View className="m-4 flex flex-col gap-2 p-2"> <Button className="w-full" - label="Copy Diagnostics Result" onPress={async () => { await Clipboard.setStringAsync(text); }} - /> + > + <Text>Copy Diagnostics Result</Text> + </Button> <Button className="w-full" - label="Retry" + variant="secondary" onPress={() => { setText(""); setRandomId(Math.random()); }} - /> + > + <Text>Retry</Text> + </Button> <View className={cn( "w-full rounded-md p-2", diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js index f3c649bb..c8d46f96 100644 --- a/apps/mobile/babel.config.js +++ b/apps/mobile/babel.config.js @@ -1,9 +1,12 @@ module.exports = function (api) { api.cache(true); + const plugins = []; + plugins.push("react-native-reanimated/plugin"); return { presets: [ ["babel-preset-expo", { jsxImportSource: "nativewind" }], "nativewind/babel", ], + plugins, }; }; diff --git a/apps/mobile/components/FullPageError.tsx b/apps/mobile/components/FullPageError.tsx index 57fd62ed..f340d052 100644 --- a/apps/mobile/components/FullPageError.tsx +++ b/apps/mobile/components/FullPageError.tsx @@ -1,4 +1,5 @@ -import { Text, View } from "react-native"; +import { View } from "react-native"; +import { Text } from "@/components/ui/Text"; import { Button } from "./ui/Button"; @@ -16,7 +17,9 @@ export default function FullPageError({ Something Went Wrong </Text> <Text className="text-foreground"> {error}</Text> - <Button onPress={() => onRetry()} label="Retry" /> + <Button onPress={onRetry}> + <Text>Retry</Text> + </Button> </View> </View> ); diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx index 461967b4..e4c2eee8 100644 --- a/apps/mobile/components/bookmarks/BookmarkCard.tsx +++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx @@ -7,7 +7,6 @@ import { Pressable, ScrollView, Share, - Text, View, } from "react-native"; import * as Clipboard from "expo-clipboard"; @@ -15,6 +14,7 @@ import * as FileSystem from "expo-file-system"; import * as Haptics from "expo-haptics"; import { router, useRouter } from "expo-router"; import * as Sharing from "expo-sharing"; +import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; import { MenuView } from "@react-native-menu/menu"; @@ -332,9 +332,7 @@ function LinkCard({ <TagList bookmark={bookmark} /> <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> <View className="mt-2 flex flex-row justify-between px-2 pb-2"> - <Text className="my-auto line-clamp-1 text-foreground"> - {parsedUrl.host} - </Text> + <Text className="my-auto line-clamp-1">{parsedUrl.host}</Text> <ActionBar bookmark={bookmark} /> </View> </View> @@ -357,7 +355,7 @@ function TextCard({ <View className="flex max-h-96 gap-2 p-2"> <Pressable onPress={onOpenBookmark}> {bookmark.title && ( - <Text className="line-clamp-2 text-xl font-bold text-foreground"> + <Text className="line-clamp-2 text-xl font-bold"> {bookmark.title} </Text> )} @@ -404,9 +402,7 @@ function AssetCard({ <View className="flex gap-2 p-2"> <Pressable onPress={onOpenBookmark}> {title && ( - <Text className="line-clamp-2 text-xl font-bold text-foreground"> - {title} - </Text> + <Text className="line-clamp-2 text-xl font-bold">{title}</Text> )} </Pressable> <TagList bookmark={bookmark} /> @@ -481,9 +477,5 @@ export default function BookmarkCard({ break; } - return ( - <View className="overflow-hidden rounded-xl border-b border-accent bg-background"> - {comp} - </View> - ); + return <View className="overflow-hidden rounded-xl bg-card">{comp}</View>; } diff --git a/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx b/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx index c4a059cc..730bcd08 100644 --- a/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx +++ b/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx @@ -1,11 +1,12 @@ import { useState } from "react"; -import { Pressable, Text, View } from "react-native"; +import { Pressable, View } from "react-native"; import ImageView from "react-native-image-viewing"; import WebView from "react-native-webview"; import { WebViewSourceUri } from "react-native-webview/lib/WebViewTypes"; +import { Text } from "@/components/ui/Text"; import { useAssetUrl } from "@/lib/hooks"; import { api } from "@/lib/trpc"; -import { useColorScheme } from "nativewind"; +import { useColorScheme } from "@/lib/useColorScheme"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -36,7 +37,7 @@ export function BookmarkLinkReaderPreview({ }: { bookmark: ZBookmark; }) { - const { colorScheme } = useColorScheme(); + const { isDarkColorScheme: isDark } = useColorScheme(); const { data: bookmarkWithContent, @@ -60,8 +61,6 @@ export function BookmarkLinkReaderPreview({ throw new Error("Wrong content type rendered"); } - const isDark = colorScheme === "dark"; - return ( <View className="flex-1 bg-background"> <WebView diff --git a/apps/mobile/components/bookmarks/BookmarkList.tsx b/apps/mobile/components/bookmarks/BookmarkList.tsx index 7be63ed6..adcf12e0 100644 --- a/apps/mobile/components/bookmarks/BookmarkList.tsx +++ b/apps/mobile/components/bookmarks/BookmarkList.tsx @@ -1,6 +1,7 @@ import { useRef } from "react"; -import { ActivityIndicator, Keyboard, Text, View } from "react-native"; +import { ActivityIndicator, Keyboard, View } from "react-native"; import Animated, { LinearTransition } from "react-native-reanimated"; +import { Text } from "@/components/ui/Text"; import { useScrollToTop } from "@react-navigation/native"; import type { ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -38,7 +39,7 @@ export default function BookmarkList({ renderItem={(b) => <BookmarkCard bookmark={b.item} />} ListEmptyComponent={ <View className="items-center justify-center pt-4"> - <Text className="text-xl text-foreground">No Bookmarks</Text> + <Text variant="title3">No Bookmarks</Text> </View> } data={bookmarks} diff --git a/apps/mobile/components/bookmarks/PDFViewer.tsx b/apps/mobile/components/bookmarks/PDFViewer.tsx index 24b9edfb..c6412431 100644 --- a/apps/mobile/components/bookmarks/PDFViewer.tsx +++ b/apps/mobile/components/bookmarks/PDFViewer.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useMemo, useState } from "react"; -import { ActivityIndicator, StyleSheet, Text, View } from "react-native"; +import { ActivityIndicator, StyleSheet, View } from "react-native"; import ReactNativeBlobUtil from "react-native-blob-util"; import Pdf from "react-native-pdf"; +import { Text } from "@/components/ui/Text"; import { useQuery } from "@tanstack/react-query"; import { useColorScheme } from "nativewind"; diff --git a/apps/mobile/components/bookmarks/TagPill.tsx b/apps/mobile/components/bookmarks/TagPill.tsx index eb9945e5..caf0f636 100644 --- a/apps/mobile/components/bookmarks/TagPill.tsx +++ b/apps/mobile/components/bookmarks/TagPill.tsx @@ -7,7 +7,7 @@ export default function TagPill({ tag }: { tag: ZBookmarkTags }) { return ( <View key={tag.id} - className="rounded-full border border-accent px-2.5 py-0.5 text-xs font-semibold" + className="rounded-full border border-input px-2.5 py-0.5 text-xs font-semibold" > <Link className="text-foreground" href={`dashboard/tags/${tag.id}`}> {tag.name} diff --git a/apps/mobile/components/ui/Button.tsx b/apps/mobile/components/ui/Button.tsx index 0f3b4ab3..312c3129 100644 --- a/apps/mobile/components/ui/Button.tsx +++ b/apps/mobile/components/ui/Button.tsx @@ -1,81 +1,200 @@ import type { VariantProps } from "class-variance-authority"; -import { Text, TouchableOpacity } from "react-native"; +import * as React from "react"; +import { + Platform, + Pressable, + PressableProps, + View, + ViewStyle, +} from "react-native"; +import { TextClassContext } from "@/components/ui/Text"; +import { useColorScheme } from "@/lib/useColorScheme"; import { cn } from "@/lib/utils"; +import { COLORS } from "@/theme/colors"; +import * as Slot from "@rn-primitives/slot"; import { cva } from "class-variance-authority"; -const buttonVariants = cva( - "flex flex-row items-center justify-center rounded-md", - { - variants: { - variant: { - default: "bg-primary", - secondary: "bg-secondary", - destructive: "bg-destructive", - ghost: "bg-slate-700", - link: "text-primary underline-offset-4", - }, - size: { - default: "h-10 px-4", - sm: "h-8 px-2", - lg: "h-12 px-8", - }, +const buttonVariants = cva("flex-row items-center justify-center gap-2", { + variants: { + variant: { + primary: "ios:active:opacity-80 bg-primary", + secondary: + "ios:border-primary ios:active:bg-primary/5 border border-foreground/40", + tonal: + "ios:bg-primary/10 dark:ios:bg-primary/10 ios:active:bg-primary/15 bg-primary/15 dark:bg-primary/30", + plain: "ios:active:opacity-70", + destructive: + "ios:bg-destructive border border-destructive/5 bg-destructive/80", }, - defaultVariants: { - variant: "default", - size: "default", + size: { + none: "", + sm: "rounded-full px-2.5 py-1", + md: "ios:rounded-lg ios:py-1.5 ios:px-3.5 rounded-full px-5 py-2", + lg: "ios:py-2 gap-2 rounded-xl px-5 py-2.5", + icon: "ios:rounded-lg h-10 w-10 rounded-full", }, }, -); + defaultVariants: { + variant: "primary", + size: "md", + }, +}); + +const androidRootVariants = cva("overflow-hidden", { + variants: { + size: { + none: "", + icon: "rounded-full", + sm: "rounded-full", + md: "rounded-full", + lg: "rounded-xl", + }, + }, + defaultVariants: { + size: "md", + }, +}); -const buttonTextVariants = cva("text-center font-medium", { +const buttonTextVariants = cva("font-medium", { variants: { variant: { - default: "text-primary-foreground", - secondary: "text-secondary-foreground", - destructive: "text-destructive-foreground", - ghost: "text-primary-foreground", - link: "text-primary-foreground underline", + primary: "text-white", + secondary: "ios:text-primary text-foreground", + tonal: "ios:text-primary text-foreground", + plain: "text-foreground", + destructive: "text-white", }, size: { - default: "text-base", - sm: "text-sm", - lg: "text-xl", + none: "", + icon: "", + sm: "text-[15px] leading-5", + md: "text-[17px] leading-7", + lg: "text-[17px] leading-7", }, }, defaultVariants: { - variant: "default", - size: "default", + variant: "primary", + size: "md", }, }); -interface ButtonProps - extends React.ComponentPropsWithoutRef<typeof TouchableOpacity>, - VariantProps<typeof buttonVariants> { - label: string; - labelClasses?: string; +function convertToRGBA(rgb: string, opacity: number): string { + const rgbValues = rgb.match(/\d+/g); + if (!rgbValues || rgbValues.length !== 3) { + throw new Error("Invalid RGB color format"); + } + const red = parseInt(rgbValues[0], 10); + const green = parseInt(rgbValues[1], 10); + const blue = parseInt(rgbValues[2], 10); + if (opacity < 0 || opacity > 1) { + throw new Error("Opacity must be a number between 0 and 1"); + } + return `rgba(${red},${green},${blue},${opacity})`; } -function Button({ - label, - labelClasses, - className, - variant, - size, - ...props -}: ButtonProps) { - return ( - <TouchableOpacity - className={cn(buttonVariants({ variant, size, className }))} - {...props} - > - <Text - className={cn( - buttonTextVariants({ variant, size, className: labelClasses }), - )} - > - {label} - </Text> - </TouchableOpacity> - ); + +const ANDROID_RIPPLE = { + dark: { + primary: { + color: convertToRGBA(COLORS.dark.grey3, 0.4), + borderless: false, + }, + secondary: { + color: convertToRGBA(COLORS.dark.grey5, 0.8), + borderless: false, + }, + plain: { color: convertToRGBA(COLORS.dark.grey5, 0.8), borderless: false }, + tonal: { color: convertToRGBA(COLORS.dark.grey5, 0.8), borderless: false }, + destructive: { + color: convertToRGBA(COLORS.dark.destructive, 0.8), + borderless: false, + }, + }, + light: { + primary: { + color: convertToRGBA(COLORS.light.grey4, 0.4), + borderless: false, + }, + secondary: { + color: convertToRGBA(COLORS.light.grey5, 0.4), + borderless: false, + }, + plain: { color: convertToRGBA(COLORS.light.grey5, 0.4), borderless: false }, + tonal: { color: convertToRGBA(COLORS.light.grey6, 0.4), borderless: false }, + destructive: { + color: convertToRGBA(COLORS.light.destructive, 0.4), + borderless: false, + }, + }, +}; + +// Add as class when possible: https://github.com/marklawlor/nativewind/issues/522 +const BORDER_CURVE: ViewStyle = { + borderCurve: "continuous", +}; + +type ButtonVariantProps = Omit< + VariantProps<typeof buttonVariants>, + "variant" +> & { + variant?: Exclude<VariantProps<typeof buttonVariants>["variant"], null>; +}; + +interface AndroidOnlyButtonProps { + /** + * ANDROID ONLY: The class name of root responsible for hidding the ripple overflow. + */ + androidRootClassName?: string; } -export { Button, buttonVariants, buttonTextVariants }; +type ButtonProps = PressableProps & ButtonVariantProps & AndroidOnlyButtonProps; + +const Root = Platform.OS === "android" ? View : Slot.Pressable; + +const Button = React.forwardRef< + React.ElementRef<typeof Pressable>, + ButtonProps +>( + ( + { + className, + variant = "primary", + size, + style = BORDER_CURVE, + androidRootClassName, + ...props + }, + ref, + ) => { + const { colorScheme } = useColorScheme(); + + return ( + <TextClassContext.Provider value={buttonTextVariants({ variant, size })}> + <Root + className={Platform.select({ + ios: androidRootClassName, + default: androidRootVariants({ + size, + className: androidRootClassName, + }), + })} + > + <Pressable + className={cn( + props.disabled && "opacity-50", + buttonVariants({ variant, size, className }), + )} + ref={ref} + style={style} + android_ripple={ANDROID_RIPPLE[colorScheme][variant]} + {...props} + /> + </Root> + </TextClassContext.Provider> + ); + }, +); + +Button.displayName = "Button"; + +export { Button, buttonTextVariants, buttonVariants }; +export type { ButtonProps }; diff --git a/apps/mobile/components/ui/ChevronRight.tsx b/apps/mobile/components/ui/ChevronRight.tsx new file mode 100644 index 00000000..5b9af6e1 --- /dev/null +++ b/apps/mobile/components/ui/ChevronRight.tsx @@ -0,0 +1,11 @@ +import { useColorScheme } from "@/lib/useColorScheme"; +import { ChevronRightIcon } from "lucide-react-native"; + +export default function ChevronRight({ + color, + ...props +}: React.ComponentProps<typeof ChevronRightIcon>) { + const { colors } = useColorScheme(); + + return <ChevronRightIcon color={color ?? colors.grey} {...props} />; +} diff --git a/apps/mobile/components/ui/Divider.tsx b/apps/mobile/components/ui/Divider.tsx index fbc5cf64..bcc6144f 100644 --- a/apps/mobile/components/ui/Divider.tsx +++ b/apps/mobile/components/ui/Divider.tsx @@ -12,7 +12,7 @@ function Divider({ return ( <View className={cn( - "bg-accent", + "bg-slate-400/20 dark:bg-border/50", orientation === "horizontal" ? "h-0.5" : "w-0.5", className, )} diff --git a/apps/mobile/components/ui/Input.tsx b/apps/mobile/components/ui/Input.tsx index 2bd5e190..7f3a48e5 100644 --- a/apps/mobile/components/ui/Input.tsx +++ b/apps/mobile/components/ui/Input.tsx @@ -1,10 +1,9 @@ import type { TextInputProps } from "react-native"; import { forwardRef } from "react"; -import { ActivityIndicator, Text, TextInput, View } from "react-native"; +import { ActivityIndicator, TextInput, View } from "react-native"; +import { Text } from "@/components/ui/Text"; import { cn } from "@/lib/utils"; -import { TailwindResolver } from "../TailwindResolver"; - export interface InputProps extends TextInputProps { label?: string; labelClasses?: string; @@ -22,20 +21,14 @@ export const Input = forwardRef<TextInput, InputProps>( {label && ( <Text className={cn("text-base", labelClasses)}>{label}</Text> )} - <TailwindResolver - className="text-gray-400" - comp={(styles) => ( - <TextInput - ref={ref} - placeholderTextColor={styles?.color?.toString()} - className={cn( - "bg-background text-foreground", - inputClasses, - "rounded-lg border border-input px-4 py-2.5", - )} - {...props} - /> + <TextInput + ref={ref} + className={cn( + "flex h-10 w-full min-w-0 flex-row items-center rounded-md border border-input text-base leading-5 text-foreground shadow-sm shadow-black/5 dark:bg-input/30 sm:h-9", + "rounded-lg border border-input px-4 py-2.5 placeholder:text-muted-foreground/50", + inputClasses, )} + {...props} /> {loading && ( <ActivityIndicator className="absolute bottom-0 right-0 p-2" /> diff --git a/apps/mobile/components/ui/List.tsx b/apps/mobile/components/ui/List.tsx new file mode 100644 index 00000000..52ff5779 --- /dev/null +++ b/apps/mobile/components/ui/List.tsx @@ -0,0 +1,469 @@ +import type { + FlashListProps, + ListRenderItem as FlashListRenderItem, + ListRenderItemInfo, +} from "@shopify/flash-list"; +import * as React from "react"; +import { + Platform, + PressableProps, + StyleProp, + TextStyle, + View, + ViewProps, + ViewStyle, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Button } from "@/components/ui/Button"; +import { Text, TextClassContext } from "@/components/ui/Text"; +import { cn } from "@/lib/utils"; +import { FlashList } from "@shopify/flash-list"; +import { cva } from "class-variance-authority"; +import { cssInterop } from "nativewind"; + +cssInterop(FlashList, { + className: "style", + contentContainerClassName: "contentContainerStyle", +}); + +type ListDataItem = string | { title: string; subTitle?: string }; +type ListVariant = "insets" | "full-width"; + +type ListRef<T extends ListDataItem> = React.Ref<typeof FlashList<T>>; + +type ListRenderItemProps<T extends ListDataItem> = ListRenderItemInfo<T> & { + variant?: ListVariant; + isFirstInSection?: boolean; + isLastInSection?: boolean; + sectionHeaderAsGap?: boolean; +}; + +type ListProps<T extends ListDataItem> = Omit< + FlashListProps<T>, + "renderItem" +> & { + renderItem?: ListRenderItem<T>; + variant?: ListVariant; + sectionHeaderAsGap?: boolean; + rootClassName?: string; + rootStyle?: StyleProp<ViewStyle>; +}; +type ListRenderItem<T extends ListDataItem> = ( + props: ListRenderItemProps<T>, +) => ReturnType<FlashListRenderItem<T>>; + +const rootVariants = cva("min-h-2 flex-1", { + variants: { + variant: { + insets: "ios:px-4", + "full-width": "ios:bg-card ios:dark:bg-background", + }, + sectionHeaderAsGap: { + true: "", + false: "", + }, + }, + compoundVariants: [ + { + variant: "full-width", + sectionHeaderAsGap: true, + className: "bg-card dark:bg-background", + }, + ], + defaultVariants: { + variant: "full-width", + sectionHeaderAsGap: false, + }, +}); + +function ListComponent<T extends ListDataItem>({ + variant = "full-width", + rootClassName, + rootStyle, + contentContainerClassName, + renderItem, + data, + sectionHeaderAsGap = false, + contentInsetAdjustmentBehavior = "automatic", + ...props +}: ListProps<T>) { + const insets = useSafeAreaInsets(); + return ( + <View + className={cn( + rootVariants({ + variant, + sectionHeaderAsGap, + }), + rootClassName, + )} + style={rootStyle} + > + <FlashList + data={data} + contentInsetAdjustmentBehavior={contentInsetAdjustmentBehavior} + renderItem={renderItemWithVariant( + renderItem, + variant, + data, + sectionHeaderAsGap, + )} + contentContainerClassName={cn( + variant === "insets" && + (!data || (typeof data?.[0] !== "string" && "pt-4")), + contentContainerClassName, + )} + contentContainerStyle={{ + paddingBottom: Platform.select({ + ios: + !contentInsetAdjustmentBehavior || + contentInsetAdjustmentBehavior === "never" + ? insets.bottom + 16 + : 0, + default: insets.bottom, + }), + }} + getItemType={getItemType} + showsVerticalScrollIndicator={false} + {...props} + /> + </View> + ); +} + +function getItemType<T>(item: T) { + return typeof item === "string" ? "sectioHeader" : "row"; +} + +function renderItemWithVariant<T extends ListDataItem>( + renderItem: ListRenderItem<T> | null | undefined, + variant: ListVariant, + data: readonly T[] | null | undefined, + sectionHeaderAsGap?: boolean, +) { + return (args: ListRenderItemProps<T>) => { + const previousItem = data?.[args.index - 1]; + const nextItem = data?.[args.index + 1]; + return renderItem + ? renderItem({ + ...args, + variant, + isFirstInSection: !previousItem || typeof previousItem === "string", + isLastInSection: !nextItem || typeof nextItem === "string", + sectionHeaderAsGap, + }) + : null; + }; +} + +const List = React.forwardRef(ListComponent) as <T extends ListDataItem>( + props: ListProps<T> & { ref?: ListRef<T> }, +) => React.ReactElement; + +function isPressable(props: PressableProps) { + return ( + ("onPress" in props && props.onPress) || + ("onLongPress" in props && props.onLongPress) || + ("onPressIn" in props && props.onPressIn) || + ("onPressOut" in props && props.onPressOut) || + ("onLongPress" in props && props.onLongPress) + ); +} + +type ListItemProps<T extends ListDataItem> = PressableProps & + ListRenderItemProps<T> & { + androidRootClassName?: string; + titleClassName?: string; + titleStyle?: StyleProp<TextStyle>; + textNumberOfLines?: number; + subTitleClassName?: string; + subTitleStyle?: StyleProp<TextStyle>; + subTitleNumberOfLines?: number; + textContentClassName?: string; + leftView?: React.ReactNode; + rightView?: React.ReactNode; + removeSeparator?: boolean; + }; +type ListItemRef = React.Ref<View>; + +const itemVariants = cva("ios:gap-0 flex-row gap-0 bg-card", { + variants: { + variant: { + insets: "ios:bg-card bg-card/70", + "full-width": "bg-card dark:bg-background", + }, + sectionHeaderAsGap: { + true: "", + false: "", + }, + isFirstItem: { + true: "", + false: "", + }, + isFirstInSection: { + true: "", + false: "", + }, + removeSeparator: { + true: "", + false: "", + }, + isLastInSection: { + true: "", + false: "", + }, + disabled: { + true: "opacity-70", + false: "opacity-100", + }, + }, + compoundVariants: [ + { + variant: "insets", + sectionHeaderAsGap: true, + className: "ios:dark:bg-card dark:bg-card/70", + }, + { + variant: "insets", + isFirstInSection: true, + className: "ios:rounded-t-[10px]", + }, + { + variant: "insets", + isLastInSection: true, + className: "ios:rounded-b-[10px]", + }, + { + removeSeparator: false, + isLastInSection: true, + className: + "ios:border-b-0 border-b border-border/25 dark:border-border/80", + }, + { + variant: "insets", + isFirstItem: true, + className: "border-t border-border/40", + }, + ], + defaultVariants: { + variant: "insets", + sectionHeaderAsGap: false, + isFirstInSection: false, + isLastInSection: false, + disabled: false, + }, +}); + +function ListItemComponent<T extends ListDataItem>( + { + item, + isFirstInSection, + isLastInSection, + index: _index, + variant, + className, + androidRootClassName, + titleClassName, + titleStyle, + textNumberOfLines, + subTitleStyle, + subTitleClassName, + subTitleNumberOfLines, + textContentClassName, + sectionHeaderAsGap, + removeSeparator = false, + leftView, + rightView, + disabled, + ...props + }: ListItemProps<T>, + ref: ListItemRef, +) { + if (typeof item === "string") { + console.log( + "List.tsx", + "ListItemComponent", + "Invalid item of type 'string' was provided. Use ListSectionHeader instead.", + ); + return null; + } + return ( + <> + <Button + disabled={disabled || !isPressable(props)} + variant="plain" + size="none" + unstable_pressDelay={100} + androidRootClassName={androidRootClassName} + className={itemVariants({ + variant, + sectionHeaderAsGap, + isFirstInSection, + isLastInSection, + disabled, + className, + removeSeparator, + })} + {...props} + ref={ref} + > + <TextClassContext.Provider value="font-normal leading-5"> + {!!leftView && <View>{leftView}</View>} + <View + className={cn( + "h-full flex-1 flex-row", + !item.subTitle ? "ios:py-3 py-[18px]" : "ios:py-2 py-2", + !leftView && "ml-4", + !rightView && "pr-4", + !removeSeparator && + (!isLastInSection || variant === "full-width") && + "ios:border-b ios:border-border/80", + !removeSeparator && + isFirstInSection && + variant === "full-width" && + "ios:border-t ios:border-border/80", + )} + > + <View className={cn("flex-1", textContentClassName)}> + <Text + numberOfLines={textNumberOfLines} + style={titleStyle} + className={titleClassName} + > + {item.title} + </Text> + {!!item.subTitle && ( + <Text + numberOfLines={subTitleNumberOfLines} + variant="subhead" + style={subTitleStyle} + className={cn("text-muted-foreground", subTitleClassName)} + > + {item.subTitle} + </Text> + )} + </View> + {!!rightView && <View>{rightView}</View>} + </View> + </TextClassContext.Provider> + </Button> + {!removeSeparator && Platform.OS !== "ios" && !isLastInSection && ( + <View className={cn(variant === "insets" && "px-4")}> + <View className="h-px bg-border/25 dark:bg-border/80" /> + </View> + )} + </> + ); +} + +const ListItem = React.forwardRef(ListItemComponent) as < + T extends ListDataItem, +>( + props: ListItemProps<T> & { ref?: ListItemRef }, +) => React.ReactElement; + +type ListSectionHeaderProps<T extends ListDataItem> = ViewProps & + ListRenderItemProps<T> & { + textClassName?: string; + }; +type ListSectionHeaderRef = React.Ref<View>; + +function ListSectionHeaderComponent<T extends ListDataItem>( + { + item, + isFirstInSection: _isFirstInSection, + isLastInSection: _isLastInSection, + index: _index, + variant, + className, + textClassName, + sectionHeaderAsGap, + ...props + }: ListSectionHeaderProps<T>, + ref: ListSectionHeaderRef, +) { + if (typeof item !== "string") { + console.log( + "List.tsx", + "ListSectionHeaderComponent", + "Invalid item provided. Expected type 'string'. Use ListItem instead.", + ); + return null; + } + + if (sectionHeaderAsGap) { + return ( + <View + className={cn( + "bg-background", + Platform.OS !== "ios" && + "border-b border-border/25 dark:border-border/80", + className, + )} + {...props} + ref={ref} + > + <View className="h-8" /> + </View> + ); + } + return ( + <View + className={cn( + "ios:pb-1 pb-4 pl-4 pt-4", + Platform.OS !== "ios" && + "border-b border-border/25 dark:border-border/80", + variant === "full-width" + ? "bg-card dark:bg-background" + : "bg-background", + className, + )} + {...props} + ref={ref} + > + <Text + variant={Platform.select({ ios: "footnote", default: "body" })} + className={cn("ios:uppercase ios:text-muted-foreground", textClassName)} + > + {item} + </Text> + </View> + ); +} + +const ListSectionHeader = React.forwardRef(ListSectionHeaderComponent) as < + T extends ListDataItem, +>( + props: ListSectionHeaderProps<T> & { ref?: ListSectionHeaderRef }, +) => React.ReactElement; + +const ESTIMATED_ITEM_HEIGHT = { + titleOnly: Platform.select({ ios: 45, default: 57 }), + withSubTitle: 56, +}; + +function getStickyHeaderIndices<T extends ListDataItem>(data: T[]) { + if (!data) return []; + const indices: number[] = []; + for (let i = 0; i < data.length; i++) { + if (typeof data[i] === "string") { + indices.push(i); + } + } + return indices; +} + +export { + ESTIMATED_ITEM_HEIGHT, + List, + ListItem, + ListSectionHeader, + getStickyHeaderIndices, +}; +export type { + ListDataItem, + ListItemProps, + ListProps, + ListRenderItemInfo, + ListSectionHeaderProps, +}; diff --git a/apps/mobile/components/ui/PageTitle.tsx b/apps/mobile/components/ui/PageTitle.tsx index dc712379..28afa408 100644 --- a/apps/mobile/components/ui/PageTitle.tsx +++ b/apps/mobile/components/ui/PageTitle.tsx @@ -1,4 +1,4 @@ -import { Text } from "react-native"; +import { Text } from "@/components/ui/Text"; import { cx } from "class-variance-authority"; export default function PageTitle({ diff --git a/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx b/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx new file mode 100644 index 00000000..969e48b2 --- /dev/null +++ b/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx @@ -0,0 +1,187 @@ +import type { + NativeSyntheticEvent, + TextInputFocusEventData, +} from "react-native"; +import * as React from "react"; +import { Pressable, TextInput, View, ViewStyle } from "react-native"; +import Animated, { + measure, + useAnimatedRef, + useAnimatedStyle, + useDerivedValue, + withTiming, +} from "react-native-reanimated"; +import { Text } from "@/components/ui/Text"; +import { useColorScheme } from "@/lib/useColorScheme"; +import { cn } from "@/lib/utils"; +import { useAugmentedRef, useControllableState } from "@rn-primitives/hooks"; +import { Icon } from "@roninoss/icons"; + +import type { SearchInputProps } from "./types"; + +// Add as class when possible: https://github.com/marklawlor/nativewind/issues/522 +const BORDER_CURVE: ViewStyle = { + borderCurve: "continuous", +}; + +const SearchInput = React.forwardRef< + React.ElementRef<typeof TextInput>, + SearchInputProps +>( + ( + { + value: valueProp, + onChangeText: onChangeTextProp, + onFocus: onFocusProp, + placeholder = "Search...", + cancelText = "Cancel", + containerClassName, + iconContainerClassName, + className, + iconColor, + onCancel, + ...props + }, + ref, + ) => { + const { colors } = useColorScheme(); + const inputRef = useAugmentedRef({ ref, methods: { focus, blur, clear } }); + const [showCancel, setShowCancel] = React.useState(false); + const showCancelDerivedValue = useDerivedValue( + () => showCancel, + [showCancel], + ); + const animatedRef = useAnimatedRef(); + + const [value = "", onChangeText] = useControllableState({ + prop: valueProp, + defaultProp: valueProp ?? "", + onChange: onChangeTextProp, + }); + + const rootStyle = useAnimatedStyle(() => { + if (_WORKLET) { + // safely use measure + const measurement = measure(animatedRef); + return { + paddingRight: showCancelDerivedValue.value + ? withTiming(measurement?.width ?? cancelText.length * 11.2) + : withTiming(0), + }; + } + return { + paddingRight: showCancelDerivedValue.value + ? withTiming(cancelText.length * 11.2) + : withTiming(0), + }; + }); + const buttonStyle3 = useAnimatedStyle(() => { + if (_WORKLET) { + // safely use measure + const measurement = measure(animatedRef); + return { + position: "absolute", + right: 0, + opacity: showCancelDerivedValue.value ? withTiming(1) : withTiming(0), + transform: [ + { + translateX: showCancelDerivedValue.value + ? withTiming(0) + : measurement?.width + ? withTiming(measurement.width) + : cancelText.length * 11.2, + }, + ], + }; + } + return { + position: "absolute", + right: 0, + opacity: showCancelDerivedValue.value ? withTiming(1) : withTiming(0), + transform: [ + { + translateX: showCancelDerivedValue.value + ? withTiming(0) + : withTiming(cancelText.length * 11.2), + }, + ], + }; + }); + + function focus() { + inputRef.current?.focus(); + } + + function blur() { + inputRef.current?.blur(); + } + + function clear() { + onChangeText(""); + } + + function onFocus(e: NativeSyntheticEvent<TextInputFocusEventData>) { + setShowCancel(true); + onFocusProp?.(e); + } + + return ( + <Animated.View className="flex-row items-center" style={rootStyle}> + <Animated.View + style={BORDER_CURVE} + className={cn( + "flex-1 flex-row rounded-lg bg-card", + containerClassName, + )} + > + <View + className={cn( + "absolute bottom-0 left-0 top-0 z-50 justify-center pl-1.5", + iconContainerClassName, + )} + > + <Icon color={iconColor ?? colors.grey3} name="magnify" size={22} /> + </View> + <TextInput + ref={inputRef} + placeholder={placeholder} + className={cn( + !showCancel && "active:bg-muted/5 dark:active:bg-muted/20", + "flex-1 rounded-lg py-2 pl-8 pr-1 text-[17px] text-foreground", + className, + )} + value={value} + onChangeText={onChangeText} + onFocus={onFocus} + clearButtonMode="while-editing" + role="searchbox" + {...props} + /> + </Animated.View> + <Animated.View + ref={animatedRef} + style={buttonStyle3} + pointerEvents={!showCancel ? "none" : "auto"} + > + <Pressable + onPress={() => { + onChangeText(""); + inputRef.current?.blur(); + setShowCancel(false); + onCancel?.(); + }} + disabled={!showCancel} + pointerEvents={!showCancel ? "none" : "auto"} + className="flex-1 justify-center active:opacity-50" + > + <Text className="px-2 text-primary">{cancelText}</Text> + </Pressable> + </Animated.View> + </Animated.View> + ); + }, +); + +SearchInput.displayName = "SearchInput"; + +export { SearchInput }; diff --git a/apps/mobile/components/ui/SearchInput/SearchInput.tsx b/apps/mobile/components/ui/SearchInput/SearchInput.tsx new file mode 100644 index 00000000..7e816ab6 --- /dev/null +++ b/apps/mobile/components/ui/SearchInput/SearchInput.tsx @@ -0,0 +1,114 @@ +import * as React from "react"; +import { Pressable, TextInput, View } from "react-native"; +import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; +import { TailwindResolver } from "@/components/TailwindResolver"; +import { Button } from "@/components/ui/Button"; +import { useColorScheme } from "@/lib/useColorScheme"; +import { cn } from "@/lib/utils"; +import { useAugmentedRef, useControllableState } from "@rn-primitives/hooks"; +import { Icon } from "@roninoss/icons"; + +import type { SearchInputProps } from "./types"; + +const SearchInput = React.forwardRef< + React.ElementRef<typeof TextInput>, + SearchInputProps +>( + ( + { + value: valueProp, + onChangeText: onChangeTextProp, + placeholder = "Search...", + containerClassName, + iconContainerClassName, + className, + onCancel, + ...props + }, + ref, + ) => { + const { colors } = useColorScheme(); + const inputRef = useAugmentedRef({ ref, methods: { focus, blur, clear } }); + const [value = "", onChangeText] = useControllableState({ + prop: valueProp, + defaultProp: valueProp ?? "", + onChange: onChangeTextProp, + }); + + function focus() { + inputRef.current?.focus(); + } + + function blur() { + inputRef.current?.blur(); + } + + function clear() { + onCancel?.(); + onChangeText(""); + } + + return ( + <Button + variant="plain" + className={cn( + "android:gap-0 android:h-14 flex-row items-center rounded-full bg-card px-2", + containerClassName, + )} + onPress={focus} + > + <View + className={cn("p-2", iconContainerClassName)} + pointerEvents="none" + > + <TailwindResolver + className="text-muted" + comp={(styles) => ( + <Icon + color={styles?.color?.toString()} + name="magnify" + size={24} + /> + )} + /> + </View> + + <View className="flex-1" pointerEvents="none"> + <TextInput + ref={inputRef} + placeholder={placeholder} + className={cn( + "flex-1 rounded-r-full p-2 text-[17px] text-foreground placeholder:text-muted", + className, + )} + placeholderTextColor={colors.foreground} + value={value} + onChangeText={onChangeText} + role="searchbox" + {...props} + /> + </View> + {!!value && ( + <Animated.View entering={FadeIn} exiting={FadeOut.duration(150)}> + <Pressable className="p-2" onPress={clear}> + <TailwindResolver + className="text-muted" + comp={(styles) => ( + <Icon + name="close" + size={24} + color={styles?.color?.toString()} + /> + )} + /> + </Pressable> + </Animated.View> + )} + </Button> + ); + }, +); + +SearchInput.displayName = "SearchInput"; + +export { SearchInput }; diff --git a/apps/mobile/components/ui/SearchInput/index.ts b/apps/mobile/components/ui/SearchInput/index.ts new file mode 100644 index 00000000..e5150fe3 --- /dev/null +++ b/apps/mobile/components/ui/SearchInput/index.ts @@ -0,0 +1 @@ +export * from "./SearchInput"; diff --git a/apps/mobile/components/ui/SearchInput/types.ts b/apps/mobile/components/ui/SearchInput/types.ts new file mode 100644 index 00000000..e0be8a2c --- /dev/null +++ b/apps/mobile/components/ui/SearchInput/types.ts @@ -0,0 +1,13 @@ +import type { TextInput, TextInputProps } from "react-native"; + +interface SearchInputProps extends TextInputProps { + containerClassName?: string; + iconContainerClassName?: string; + cancelText?: string; + iconColor?: string; + onCancel?: () => void; +} + +type SearchInputRef = TextInput; + +export type { SearchInputProps, SearchInputRef }; diff --git a/apps/mobile/components/ui/Text.tsx b/apps/mobile/components/ui/Text.tsx new file mode 100644 index 00000000..e5590c75 --- /dev/null +++ b/apps/mobile/components/ui/Text.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import { Text as RNText } from "react-native"; +import { cn } from "@/lib/utils"; +import { cva, VariantProps } from "class-variance-authority"; + +const textVariants = cva("text-foreground", { + variants: { + variant: { + largeTitle: "text-4xl", + title1: "text-2xl", + title2: "text-[22px] leading-7", + title3: "text-xl", + heading: "text-[17px] font-semibold leading-6", + body: "text-[17px] leading-6", + callout: "text-base", + subhead: "text-[15px] leading-6", + footnote: "text-[13px] leading-5", + caption1: "text-xs", + caption2: "text-[11px] leading-4", + }, + color: { + primary: "", + secondary: "text-secondary-foreground/90", + tertiary: "text-muted-foreground/90", + quarternary: "text-muted-foreground/50", + }, + }, + defaultVariants: { + variant: "body", + color: "primary", + }, +}); + +const TextClassContext = React.createContext<string | undefined>(undefined); + +function Text({ + className, + variant, + color, + ...props +}: React.ComponentPropsWithoutRef<typeof RNText> & + VariantProps<typeof textVariants>) { + const textClassName = React.useContext(TextClassContext); + return ( + <RNText + className={cn(textVariants({ variant, color }), textClassName, className)} + {...props} + /> + ); +} + +export { Text, TextClassContext, textVariants }; diff --git a/apps/mobile/components/ui/Toast.tsx b/apps/mobile/components/ui/Toast.tsx index 7bd2e64d..fd122c25 100644 --- a/apps/mobile/components/ui/Toast.tsx +++ b/apps/mobile/components/ui/Toast.tsx @@ -1,5 +1,6 @@ import { createContext, useContext, useEffect, useRef, useState } from "react"; -import { Animated, Text, View } from "react-native"; +import { Animated, View } from "react-native"; +import { Text } from "@/components/ui/Text"; import { cn } from "@/lib/utils"; const toastVariants = { diff --git a/apps/mobile/globals.css b/apps/mobile/globals.css index bf0da7e1..992b92cd 100644 --- a/apps/mobile/globals.css +++ b/apps/mobile/globals.css @@ -1 +1,131 @@ -@import "@karakeep/tailwind-config/globals"; +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 242 242 247; + --foreground: 0 0 0; + --card: 255 255 255; + --card-foreground: 0 0 0; + --popover: 230 230 235; + --popover-foreground: 0 0 0; + --primary: 0 123 255; + --primary-foreground: 255 255 255; + --secondary: 45 185 227; + --secondary-foreground: 255 255 255; + --muted: 176 176 181; + --muted-foreground: 102 102 102; + --accent: 255 40 84; + --accent-foreground: 255 255 255; + --destructive: 255 56 43; + --destructive-foreground: 255 255 255; + --border: 230 230 235; + --input: 210 210 215; + --ring: 230 230 235; + + --android-background: 250 252 255; + --android-foreground: 27 28 29; + --android-card: 255 255 255; + --android-card-foreground: 24 28 35; + --android-popover: 215 217 228; + --android-popover-foreground: 0 0 0; + --android-primary: 0 112 233; + --android-primary-foreground: 255 255 255; + --android-secondary: 176 201 255; + --android-secondary-foreground: 28 60 114; + --android-muted: 176 176 181; + --android-muted-foreground: 102 102 102; + --android-accent: 169 73 204; + --android-accent-foreground: 255 255 255; + --android-destructive: 186 26 26; + --android-destructive-foreground: 255 255 255; + --android-border: 118 122 127; + --android-input: 197 201 206; + --android-ring: 118 122 127; + + --web-background: 250 252 255; + --web-foreground: 27 28 29; + --web-card: 255 255 255; + --web-card-foreground: 24 28 35; + --web-popover: 215 217 228; + --web-popover-foreground: 0 0 0; + --web-primary: 0 112 233; + --web-primary-foreground: 255 255 255; + --web-secondary: 176 201 255; + --web-secondary-foreground: 28 60 114; + --web-muted: 216 226 255; + --web-muted-foreground: 0 26 65; + --web-accent: 169 73 204; + --web-accent-foreground: 255 255 255; + --web-destructive: 186 26 26; + --web-destructive-foreground: 255 255 255; + --web-border: 118 122 127; + --web-input: 197 201 206; + --web-ring: 118 122 127; + } + + @media (prefers-color-scheme: dark) { + :root { + --background: 0 0 0; + --foreground: 255 255 255; + --card: 21 21 24; + --card-foreground: 255 255 255; + --popover: 40 40 40; + --popover-foreground: 255 255 255; + --primary: 3 133 255; + --primary-foreground: 255 255 255; + --secondary: 100 211 254; + --secondary-foreground: 255 255 255; + --muted: 112 112 115; + --muted-foreground: 226 226 231; + --accent: 255 52 95; + --accent-foreground: 255 255 255; + --destructive: 254 67 54; + --destructive-foreground: 255 255 255; + --border: 40 40 40; + --input: 51 51 51; + --ring: 40 40 40; + + --android-background: 24 28 32; + --android-foreground: 221 227 233; + --android-card: 36 40 44; + --android-card-foreground: 197 201 206; + --android-popover: 70 74 78; + --android-popover-foreground: 197 201 206; + --android-primary: 0 69 148; + --android-primary-foreground: 214 224 255; + --android-secondary: 28 60 114; + --android-secondary-foreground: 255 255 255; + --android-muted: 112 112 115; + --android-muted-foreground: 226 226 231; + --android-accent: 83 0 111; + --android-accent-foreground: 255 255 255; + --android-destructive: 147 0 10; + --android-destructive-foreground: 255 255 255; + --android-border: 143 148 153; + --android-input: 70 74 78; + --android-ring: 143 148 153; + + --web-background: 24 28 32; + --web-foreground: 221 227 233; + --web-card: 70 74 78; + --web-card-foreground: 197 201 206; + --web-popover: 70 74 78; + --web-popover-foreground: 197 201 206; + --web-primary: 0 69 148; + --web-primary-foreground: 214 224 255; + --web-secondary: 28 60 114; + --web-secondary-foreground: 255 255 255; + --web-muted: 29 27 29; + --web-muted-foreground: 230 224 228; + --web-accent: 83 0 111; + --web-accent-foreground: 255 255 255; + --web-destructive: 147 0 10; + --web-destructive-foreground: 255 255 255; + --web-border: 143 148 153; + --web-input: 70 74 78; + --web-ring: 143 148 153; + } + } +} diff --git a/apps/mobile/lib/useColorScheme.tsx b/apps/mobile/lib/useColorScheme.tsx new file mode 100644 index 00000000..a00a445d --- /dev/null +++ b/apps/mobile/lib/useColorScheme.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; +import { Platform } from "react-native"; +import * as NavigationBar from "expo-navigation-bar"; +import useAppSettings from "@/lib/settings"; +import { COLORS } from "@/theme/colors"; +import { useColorScheme as useNativewindColorScheme } from "nativewind"; + +function useColorScheme() { + const { settings, isLoading } = useAppSettings(); + const { colorScheme, setColorScheme: setNativewindColorScheme } = + useNativewindColorScheme(); + + // Sync user settings with native color scheme + React.useEffect(() => { + setNativewindColorScheme(settings.theme); + }, [settings.theme, isLoading]); + + React.useEffect(() => { + if (Platform.OS === "android") { + setNavigationBar(colorScheme ?? "light").catch((error) => { + console.error('useColorScheme.tsx", "setColorScheme', error); + }); + } + }, [colorScheme]); + + return { + colorScheme: colorScheme ?? "light", + isDarkColorScheme: colorScheme === "dark", + colors: COLORS[colorScheme ?? "light"], + }; +} + +/** + * Set the Android navigation bar color based on the color scheme. + */ +function useInitialAndroidBarSync() { + const { colorScheme } = useColorScheme(); + React.useEffect(() => { + if (Platform.OS !== "android") return; + setNavigationBar(colorScheme).catch((error) => { + console.error('useColorScheme.tsx", "useInitialColorScheme', error); + }); + }, []); +} + +export { useColorScheme, useInitialAndroidBarSync }; + +function setNavigationBar(colorScheme: "light" | "dark") { + return Promise.all([ + NavigationBar.setButtonStyleAsync( + colorScheme === "dark" ? "light" : "dark", + ), + NavigationBar.setPositionAsync("absolute"), + NavigationBar.setBackgroundColorAsync( + colorScheme === "dark" ? "#00000030" : "#ffffff80", + ), + ]); +} diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js index f9679cb2..78f79c62 100644 --- a/apps/mobile/metro.config.js +++ b/apps/mobile/metro.config.js @@ -9,7 +9,8 @@ module.exports = withTurborepoManagedCache( // eslint-disable-next-line no-undef withNativeWind(getDefaultConfig(__dirname), { input: "./globals.css", - configPath: "./tailwind.config.ts", + configPath: "./tailwind.config.js", + inlineRem: 16, }), ), ); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 0ed5668c..61c6ec6e 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -20,6 +20,10 @@ "@karakeep/trpc": "workspace:^0.1.0", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-menu/menu": "^1.2.4", + "@rn-primitives/hooks": "^1.3.0", + "@rn-primitives/slot": "^1.2.0", + "@roninoss/icons": "^0.0.4", + "@shopify/flash-list": "^2.0.3", "@tanstack/react-query": "^5.80.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -29,7 +33,7 @@ "expo-clipboard": "^7.1.4", "expo-constants": "~17.1.6", "expo-dev-client": "^5.2.0", - "expo-file-system": "~18.0.12", + "expo-file-system": "~18.1.11", "expo-haptics": "^14.1.4", "expo-image": "^2.2.0", "expo-image-picker": "^16.1.4", @@ -50,6 +54,7 @@ "react-native-blob-util": "^0.21.2", "react-native-gesture-handler": "~2.24.0", "react-native-image-viewing": "^0.2.2", + "react-native-keyboard-controller": "^1.18.5", "react-native-markdown-display": "^7.0.2", "react-native-pdf": "^6.7.7", "react-native-reanimated": "^3.17.5", diff --git a/apps/mobile/tailwind.config.js b/apps/mobile/tailwind.config.js new file mode 100644 index 00000000..74a9f30a --- /dev/null +++ b/apps/mobile/tailwind.config.js @@ -0,0 +1,66 @@ +const { hairlineWidth, platformSelect } = require("nativewind/theme"); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + // NOTE: Update this to include the paths to all of your component files. + content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"], + presets: [require("nativewind/preset")], + theme: { + extend: { + colors: { + border: withOpacity("border"), + input: withOpacity("input"), + ring: withOpacity("ring"), + background: withOpacity("background"), + foreground: withOpacity("foreground"), + primary: { + DEFAULT: withOpacity("primary"), + foreground: withOpacity("primary-foreground"), + }, + secondary: { + DEFAULT: withOpacity("secondary"), + foreground: withOpacity("secondary-foreground"), + }, + destructive: { + DEFAULT: withOpacity("destructive"), + foreground: withOpacity("destructive-foreground"), + }, + muted: { + DEFAULT: withOpacity("muted"), + foreground: withOpacity("muted-foreground"), + }, + accent: { + DEFAULT: withOpacity("accent"), + foreground: withOpacity("accent-foreground"), + }, + popover: { + DEFAULT: withOpacity("popover"), + foreground: withOpacity("popover-foreground"), + }, + card: { + DEFAULT: withOpacity("card"), + foreground: withOpacity("card-foreground"), + }, + }, + borderWidth: { + hairline: hairlineWidth(), + }, + }, + }, + plugins: [], +}; + +function withOpacity(variableName) { + return ({ opacityValue }) => { + if (opacityValue !== undefined) { + return platformSelect({ + ios: `rgb(var(--${variableName}) / ${opacityValue})`, + android: `rgb(var(--android-${variableName}) / ${opacityValue})`, + }); + } + return platformSelect({ + ios: `rgb(var(--${variableName}))`, + android: `rgb(var(--android-${variableName}))`, + }); + }; +} diff --git a/apps/mobile/tailwind.config.ts b/apps/mobile/tailwind.config.ts deleted file mode 100644 index 03712ec4..00000000 --- a/apps/mobile/tailwind.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Config } from "tailwindcss"; - -import base from "@karakeep/tailwind-config/native"; - -const config = { - content: base.content, - plugins: [], - presets: [base, require("nativewind/preset")], -} satisfies Config; - -export default config; diff --git a/apps/mobile/theme/colors.ts b/apps/mobile/theme/colors.ts new file mode 100644 index 00000000..626bcb99 --- /dev/null +++ b/apps/mobile/theme/colors.ts @@ -0,0 +1,109 @@ +import { Platform } from "react-native"; + +const IOS_SYSTEM_COLORS = { + white: "rgb(255, 255, 255)", + black: "rgb(0, 0, 0)", + light: { + grey6: "rgb(242, 242, 247)", + grey5: "rgb(230, 230, 235)", + grey4: "rgb(210, 210, 215)", + grey3: "rgb(199, 199, 204)", + grey2: "rgb(176, 176, 181)", + grey: "rgb(153, 153, 158)", + background: "rgb(242, 242, 247)", + foreground: "rgb(0, 0, 0)", + root: "rgb(242, 242, 247)", + card: "rgb(242, 242, 247)", + destructive: "rgb(255, 56, 43)", + primary: "rgb(0, 123, 255)", + }, + dark: { + grey6: "rgb(21, 21, 24)", + grey5: "rgb(40, 40, 40)", + grey4: "rgb(51, 51, 51)", + grey3: "rgb(70, 70, 70)", + grey2: "rgb(99, 99, 99)", + grey: "rgb(158, 158, 158)", + background: "rgb(0, 0, 0)", + foreground: "rgb(255, 255, 255)", + root: "rgb(0, 0, 0)", + card: "rgb(0, 0, 0)", + destructive: "rgb(254, 67, 54)", + primary: "rgb(3, 133, 255)", + }, +} as const; + +const ANDROID_COLORS = { + white: "rgb(255, 255, 255)", + black: "rgb(0, 0, 0)", + light: { + grey6: "rgb(242, 242, 247)", + grey5: "rgb(230, 230, 235)", + grey4: "rgb(210, 210, 215)", + grey3: "rgb(199, 199, 204)", + grey2: "rgb(176, 176, 181)", + grey: "rgb(153, 153, 158)", + background: "rgb(250, 252, 255)", + foreground: "rgb(27, 28, 29)", + root: "rgb(250, 252, 255)", + card: "rgb(250, 252, 255)", + destructive: "rgb(186, 26, 26)", + primary: "rgb(0, 112, 233)", + }, + dark: { + grey6: "rgb(21, 21, 24)", + grey5: "rgb(40, 40, 40)", + grey4: "rgb(51, 51, 51)", + grey3: "rgb(70, 70, 70)", + grey2: "rgb(99, 99, 99)", + grey: "rgb(158, 158, 158)", + background: "rgb(24, 28, 32)", + foreground: "rgb(221, 227, 233)", + root: "rgb(24, 28, 32)", + card: "rgb(24, 28, 32)", + destructive: "rgb(147, 0, 10)", + primary: "rgb(0, 69, 148)", + }, +} as const; + +const WEB_COLORS = { + white: "rgb(255, 255, 255)", + black: "rgb(0, 0, 0)", + light: { + grey6: "rgb(250, 252, 255)", + grey5: "rgb(243, 247, 251)", + grey4: "rgb(236, 242, 248)", + grey3: "rgb(233, 239, 247)", + grey2: "rgb(229, 237, 245)", + grey: "rgb(226, 234, 243)", + background: "rgb(250, 252, 255)", + foreground: "rgb(27, 28, 29)", + root: "rgb(250, 252, 255)", + card: "rgb(250, 252, 255)", + destructive: "rgb(186, 26, 26)", + primary: "rgb(0, 112, 233)", + }, + dark: { + grey6: "rgb(25, 30, 36)", + grey5: "rgb(31, 38, 45)", + grey4: "rgb(35, 43, 52)", + grey3: "rgb(38, 48, 59)", + grey2: "rgb(40, 51, 62)", + grey: "rgb(44, 56, 68)", + background: "rgb(24, 28, 32)", + foreground: "rgb(221, 227, 233)", + root: "rgb(24, 28, 32)", + card: "rgb(24, 28, 32)", + destructive: "rgb(147, 0, 10)", + primary: "rgb(0, 69, 148)", + }, +} as const; + +const COLORS = + Platform.OS === "ios" + ? IOS_SYSTEM_COLORS + : Platform.OS === "android" + ? ANDROID_COLORS + : WEB_COLORS; + +export { COLORS }; diff --git a/apps/mobile/theme/index.ts b/apps/mobile/theme/index.ts new file mode 100644 index 00000000..05b42bf7 --- /dev/null +++ b/apps/mobile/theme/index.ts @@ -0,0 +1,30 @@ +import { DarkTheme, DefaultTheme } from "@react-navigation/native"; + +import { COLORS } from "./colors"; + +const NAV_THEME = { + light: { + ...DefaultTheme, + colors: { + background: COLORS.light.background, + border: COLORS.light.grey5, + card: COLORS.light.card, + notification: COLORS.light.destructive, + primary: COLORS.light.primary, + text: COLORS.black, + }, + }, + dark: { + ...DarkTheme, + colors: { + background: COLORS.dark.background, + border: COLORS.dark.grey5, + card: COLORS.dark.grey6, + notification: COLORS.dark.destructive, + primary: COLORS.dark.primary, + text: COLORS.white, + }, + }, +}; + +export { NAV_THEME }; diff --git a/packages/shared/debug.ts b/packages/shared/debug.ts new file mode 100644 index 00000000..43f11803 --- /dev/null +++ b/packages/shared/debug.ts @@ -0,0 +1,10 @@ +export function tap<T>(t: T, cb: (t: T) => void): T { + cb(t); + return t; +} + +export function debugPrint<T>(t: T): T { + return tap(t, (t) => { + console.log(t); + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5ba2e60..6dec122e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -323,6 +323,18 @@ importers: '@react-native-menu/menu': specifier: ^1.2.4 version: 1.2.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + '@rn-primitives/hooks': + specifier: ^1.3.0 + version: 1.3.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + '@rn-primitives/slot': + specifier: ^1.2.0 + version: 1.2.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + '@roninoss/icons': + specifier: ^0.0.4 + version: 0.0.4(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + '@shopify/flash-list': + specifier: ^2.0.3 + version: 2.0.3(@babel/runtime@7.28.3)(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) '@tanstack/react-query': specifier: ^5.80.3 version: 5.83.0(react@19.1.0) @@ -351,8 +363,8 @@ importers: specifier: ^5.2.0 version: 5.2.4(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)) expo-file-system: - specifier: ~18.0.12 - version: 18.0.12(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)) + specifier: ~18.1.11 + version: 18.1.11(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)) expo-haptics: specifier: ^14.1.4 version: 14.1.4(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)) @@ -413,6 +425,9 @@ importers: react-native-image-viewing: specifier: ^0.2.2 version: 0.2.2(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + react-native-keyboard-controller: + specifier: ^1.18.5 + version: 1.18.5(react-native-reanimated@3.18.0(@babel/core@7.26.0)(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) react-native-markdown-display: specifier: ^7.0.2 version: 7.0.2(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) @@ -3344,6 +3359,9 @@ packages: '@expo/sudo-prompt@9.3.2': resolution: {integrity: sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==} + '@expo/vector-icons@13.0.0': + resolution: {integrity: sha512-TI+l71+5aSKnShYclFa14Kum+hQMZ86b95SH6tQUG3qZEmLTarvWpKwqtTwQKqvlJSJrpFiSFu3eCuZokY6zWA==} + '@expo/vector-icons@14.1.0': resolution: {integrity: sha512-7T09UE9h8QDTsUeMGymB4i+iqvtEeaO5VvUjryFB4tugDTG/bkzViWA74hm5pfjjDEhYMXWaX112mcvhccmIwQ==} peerDependencies: @@ -4894,6 +4912,42 @@ packages: '@remusao/trie@2.1.0': resolution: {integrity: sha512-Er3Q8q0/2OcCJPQYJOPLmCuqO0wu7cav3SPtpjlxSbjFi1x+A1pZkkLD6c9q2rGEkGW/tkrRzfrhNMt8VQjzXg==} + '@rn-primitives/hooks@1.3.0': + resolution: {integrity: sha512-BR97reSu7uVDpyMeQdRJHT0w8KdS6jdYnOL6xQtqS2q3H6N7vXBlX4LFERqJZphD+aziJFIAJ3HJF1vtt6XlpQ==} + peerDependencies: + react: '*' + react-native: '*' + react-native-web: '*' + peerDependenciesMeta: + react-native: + optional: true + react-native-web: + optional: true + + '@rn-primitives/slot@1.2.0': + resolution: {integrity: sha512-cpbn+JLjSeq3wcA4uqgFsUimMrWYWx2Ks7r5rkwd1ds1utxynsGkLOKpYVQkATwWrYhtcoF1raxIKEqXuMN+/w==} + peerDependencies: + react: '*' + react-native: '*' + react-native-web: '*' + peerDependenciesMeta: + react-native: + optional: true + react-native-web: + optional: true + + '@rn-primitives/types@1.2.0': + resolution: {integrity: sha512-b+6zKgdKVqAfaFPSfhwlQL0dnPQXPpW890m3eguC0VDI1eOsoEvUfVb6lmgH4bum9MmI0xymq4tOUI/fsKLoCQ==} + peerDependencies: + react: '*' + react-native: '*' + react-native-web: '*' + peerDependenciesMeta: + react-native: + optional: true + react-native-web: + optional: true + '@rolldown/binding-android-arm64@1.0.0-beta.28': resolution: {integrity: sha512-hLb7k11KBXtO8xc7DO1OWriXWM/2FKv/R510NChqpzoI6au2aJbGUQTKJw4D8Mj7oHfY2Nwzy+sJBgWx/P8IKw==} cpu: [arm64] @@ -5111,6 +5165,12 @@ packages: cpu: [x64] os: [win32] + '@roninoss/icons@0.0.4': + resolution: {integrity: sha512-NtLj98Mk+iFk3fCcxl2XzhWWD5xHgqWdxgrI7ws8KNrOlCvNNgHCHzSFEJPipXxy1fTc0c4ZgJWZsfofKsbnWQ==} + peerDependencies: + react: '*' + react-native: '*' + '@rushstack/node-core-library@5.13.1': resolution: {integrity: sha512-5yXhzPFGEkVc9Fu92wsNJ9jlvdwz4RNb2bMso+/+TH0nMm1jDDDsOIf4l8GAkPxGuwPw5DH24RliWVfSPhlW/Q==} peerDependencies: @@ -5139,6 +5199,13 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@shopify/flash-list@2.0.3': + resolution: {integrity: sha512-jUlHuZFoPdqRCDvOqsb2YkTttRPyV8Tb/EjCx3gE2wjr4UTM+fE0Ltv9bwBg0K7yo/SxRNXaW7xu5utusRb0xA==} + peerDependencies: + '@babel/runtime': '*' + react: '*' + react-native: '*' + '@sideway/address@4.1.5': resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} @@ -8063,12 +8130,6 @@ packages: peerDependencies: expo: '*' - expo-file-system@18.0.12: - resolution: {integrity: sha512-HAkrd/mb8r+G3lJ9MzmGeuW2B+BxQR1joKfeCyY4deLl1zoZ48FrAWjgZjHK9aHUVhJ0ehzInu/NQtikKytaeg==} - peerDependencies: - expo: '*' - react-native: '*' - expo-file-system@18.1.11: resolution: {integrity: sha512-HJw/m0nVOKeqeRjPjGdvm+zBi5/NxcdPf8M8P3G2JFvH5Z8vBWqVDic2O58jnT1OFEy0XXzoH9UqFu7cHg9DTQ==} peerDependencies: @@ -12259,6 +12320,19 @@ packages: react: '*' react-native: '*' + react-native-is-edge-to-edge@1.2.1: + resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-keyboard-controller@1.18.5: + resolution: {integrity: sha512-wbYN6Tcu3G5a05dhRYBgjgd74KqoYWuUmroLpigRg9cXy5uYo7prTMIvMgvLtARQtUF7BOtFggUnzgoBOgk0TQ==} + peerDependencies: + react: '*' + react-native: '*' + react-native-reanimated: '>=3.0.0' + react-native-markdown-display@7.0.2: resolution: {integrity: sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ==} peerDependencies: @@ -13403,6 +13477,13 @@ packages: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true + sweet-sfsymbols@0.7.2: + resolution: {integrity: sha512-+Dwjb7LwHWIOVfAMgrDI5+iDtAGSXGgD3ACiqqf1gtm+eLY9uVL7gDPR6VWm5bhFPOyXQy2mijUZN70mu8l1dw==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + swr@2.3.4: resolution: {integrity: sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==} peerDependencies: @@ -17527,7 +17608,7 @@ snapshots: '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.27.1 - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.3 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.3 @@ -17989,6 +18070,8 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} + '@expo/vector-icons@13.0.0': {} + '@expo/vector-icons@14.1.0(expo-font@13.3.2(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)': dependencies: expo-font: 13.3.2(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react@19.1.0) @@ -19743,6 +19826,25 @@ snapshots: '@remusao/trie@2.1.0': {} + '@rn-primitives/hooks@1.3.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)': + dependencies: + '@rn-primitives/types': 1.2.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + react: 19.1.0 + optionalDependencies: + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0) + + '@rn-primitives/slot@1.2.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0) + + '@rn-primitives/types@1.2.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0) + '@rolldown/binding-android-arm64@1.0.0-beta.28': optional: true @@ -19898,6 +20000,15 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.42.0': optional: true + '@roninoss/icons@0.0.4(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)': + dependencies: + '@expo/vector-icons': 13.0.0 + react: 19.1.0 + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0) + sweet-sfsymbols: 0.7.2(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + transitivePeerDependencies: + - expo + '@rushstack/node-core-library@5.13.1(@types/node@22.15.30)': dependencies: ajv: 8.13.0 @@ -19939,6 +20050,13 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 + '@shopify/flash-list@2.0.3(@babel/runtime@7.28.3)(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.28.3 + react: 19.1.0 + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0) + tslib: 2.8.1 + '@sideway/address@4.1.5': dependencies: '@hapi/hoek': 9.3.0 @@ -22719,7 +22837,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.3 csstype: 3.1.3 dom-serializer@1.4.1: @@ -23295,12 +23413,6 @@ snapshots: expo: 53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) expo-dev-menu-interface: 1.10.0(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)) - expo-file-system@18.0.12(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)): - dependencies: - expo: 53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) - react-native: 0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0) - web-streams-polyfill: 3.3.3 - expo-file-system@18.1.11(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)): dependencies: expo: 53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) @@ -28429,6 +28541,18 @@ snapshots: react: 19.1.0 react-native: 0.79.3(@babel/core@7.28.0)(@types/react@19.1.11)(react@19.1.0) + react-native-is-edge-to-edge@1.2.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0) + + react-native-keyboard-controller@1.18.5(react-native-reanimated@3.18.0(@babel/core@7.26.0)(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 3.18.0(@babel/core@7.26.0)(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + react-native-markdown-display@7.0.2(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0): dependencies: css-to-react-native: 3.2.0 @@ -30042,6 +30166,12 @@ snapshots: transitivePeerDependencies: - encoding + sweet-sfsymbols@0.7.2(expo@53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0): + dependencies: + expo: 53.0.20(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0)))(react-native-webview@13.14.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@19.1.8)(react@19.1.0) + swr@2.3.4(react@19.1.0): dependencies: dequal: 2.0.3 |
