From 89c688002fc48327845d37ee2e7eb93374ee0096 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Sun, 14 Jun 2026 12:28:13 +0100 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20expo=20ui=20migration=20phase=201?= =?UTF-8?q?=20+=202=20=E2=80=94=20remove=20useColorScheme/cn=20adapter=20e?= =?UTF-8?q?xports,=20migrate=20LargeTitleHeader/SearchInput=20to=20expo-ro?= =?UTF-8?q?uter=20native=20patterns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/expo/app/(app)/(tabs)/(home)/index.tsx | 131 +++++++--------- apps/expo/app/(app)/(tabs)/profile/index.tsx | 21 ++- apps/expo/app/(app)/_layout.tsx | 8 - apps/expo/app/(app)/current-pack/[id].tsx | 5 +- apps/expo/app/(app)/demo/index.tsx | 16 +- apps/expo/app/(app)/gear-inventory.tsx | 6 +- .../(app)/messages/conversations.android.tsx | 27 ++-- .../expo/app/(app)/messages/conversations.tsx | 32 ++-- apps/expo/app/(app)/pack-categories/[id].tsx | 6 +- apps/expo/app/(app)/pack-stats/[id].tsx | 6 +- apps/expo/app/(app)/recent-packs.tsx | 5 +- .../app/(app)/season-suggestions-results.tsx | 7 +- apps/expo/app/(app)/season-suggestions.tsx | 6 +- apps/expo/app/(app)/shared-packs.tsx | 4 +- apps/expo/app/(app)/shopping-list.tsx | 5 +- apps/expo/app/(app)/trail-conditions.tsx | 34 +++-- .../app/(app)/weather-alert-preferences.tsx | 12 +- apps/expo/app/(app)/weather-alerts.tsx | 6 +- apps/expo/app/(app)/weight-analysis/[id].tsx | 6 +- apps/expo/components/ErrorState.tsx | 4 +- .../LargeTitleHeaderOverlapFixIOS.tsx | 18 --- ...LargeTitleHeaderSearchContentContainer.tsx | 14 -- apps/expo/components/Markdown.tsx | 2 +- apps/expo/components/SearchInput.tsx | 13 +- .../components/initial/ExpandableText.tsx | 2 +- .../ai-packs/screens/AIPacksScreen.tsx | 5 +- .../features/ai/components/ChatBubble.tsx | 3 +- .../ai/components/GuidesRAGGenerativeUI.tsx | 3 +- .../ai/components/LocationContext.tsx | 3 +- .../ai/components/WeatherGenerativeUI.tsx | 3 +- .../ai/screens/ReportedContentScreen.tsx | 6 +- .../catalog/components/CatalogItemImage.tsx | 2 +- .../catalog/screens/CatalogItemsScreen.tsx | 142 ++++++++---------- .../expo/features/feed/screens/FeedScreen.tsx | 25 +-- .../guides/screens/GuidesListScreen.tsx | 33 ++-- .../components/AddPackTemplateItemActions.tsx | 3 +- .../components/PackTemplateItemImage.tsx | 2 +- .../screens/PackTemplateListScreen.tsx | 40 ++--- .../utils/getPackTemplateDetailOptions.tsx | 3 +- .../getPackTemplateItemDetailOptions.tsx | 3 +- .../packs/components/AddPackItemActions.tsx | 3 +- .../packs/components/GapAnalysisModal.tsx | 3 +- .../packs/components/PackItemImage.tsx | 2 +- .../packs/screens/PackItemDetailScreen.tsx | 3 +- .../features/packs/screens/PackListScreen.tsx | 64 +++----- .../packs/utils/getPackDetailOptions.tsx | 3 +- .../packs/utils/getPackItemDetailOptions.tsx | 3 +- .../trips/components/UpcomingTripsTile.tsx | 3 +- .../features/trips/screens/TripListScreen.tsx | 35 ++--- .../trips/utils/getTripDetailOptions.tsx | 3 +- .../weather/screens/LocationSearchScreen.tsx | 1 - .../weather/screens/LocationsScreen.tsx | 24 +-- .../wildlife/screens/WildlifeScreen.tsx | 6 +- 53 files changed, 366 insertions(+), 459 deletions(-) delete mode 100644 apps/expo/components/LargeTitleHeaderOverlapFixIOS.tsx delete mode 100644 apps/expo/components/LargeTitleHeaderSearchContentContainer.tsx diff --git a/apps/expo/app/(app)/(tabs)/(home)/index.tsx b/apps/expo/app/(app)/(tabs)/(home)/index.tsx index 8874b086b7..4376fd307d 100644 --- a/apps/expo/app/(app)/(tabs)/(home)/index.tsx +++ b/apps/expo/app/(app)/(tabs)/(home)/index.tsx @@ -2,16 +2,10 @@ import type { BottomSheetModal } from '@gorhom/bottom-sheet'; import { arrayIncludes, assertIsString, objectKeys } from '@packrat/guards'; -import type { LargeTitleSearchBarMethods, ListDataItem } from '@packrat/ui/nativewindui'; -import { - LargeTitleHeader, - List, - type ListRenderItemInfo, - ListSectionHeader, -} from '@packrat/ui/nativewindui'; +import type { ListDataItem } from '@packrat/ui/nativewindui'; +import { List, type ListRenderItemInfo, ListSectionHeader } from '@packrat/ui/nativewindui'; import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; import { Icon } from 'expo-app/components/Icon'; -import { LargeTitleHeaderSearchContentContainer } from 'expo-app/components/LargeTitleHeaderSearchContentContainer'; import { appConfig, featureFlags } from 'expo-app/config'; import { AIChatTile } from 'expo-app/features/ai/components/AIChatTile'; import { ReportedContentTile } from 'expo-app/features/ai/components/ReportedContentTile'; @@ -38,8 +32,7 @@ import { WeatherTile } from 'expo-app/features/weather/components/WeatherTile'; import { WildlifeTile } from 'expo-app/features/wildlife/components/WildlifeTile'; import { cn } from 'expo-app/lib/cn'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; -import { asNonNullableRef } from 'expo-app/lib/utils/asNonNullableRef'; -import { useRouter } from 'expo-router'; +import { Stack, useRouter } from 'expo-router'; import { useIsFocused } from 'expo-router/react-navigation'; import { useEffect, useMemo, useRef, useState } from 'react'; import { FlatList, Platform, Pressable, Text, View } from 'react-native'; @@ -165,7 +158,6 @@ const DASHBOARD_GAP_PREFIX = appConfig.dashboard.gapPrefix; export default function DashboardScreen() { const [searchValue, setSearchValue] = useState(''); - const searchBarRef = useRef(null); const unlockSheetRef = useRef(null); const { t } = useTranslation(); const router = useRouter(); @@ -249,71 +241,62 @@ export default function DashboardScreen() { return ( <> - - {searchValue ? ( - { - assertIsString(item); - if (!item.startsWith(DASHBOARD_GAP_PREFIX) && arrayIncludes(TILE_NAMES, item)) { - const Component = tileInfo[item].component; - return ( - { - setSearchValue(''); - searchBarRef.current?.clearText(); - }} - > - - - ); - } - return null; - }} - ListHeaderComponent={() => - filteredTiles.length > 0 ? ( - - {filteredTiles.length}{' '} - {filteredTiles.length === 1 - ? appConfig.dashboard.strings.resultSingular - : appConfig.dashboard.strings.resultPlural} - - ) : null - } - ListEmptyComponent={() => ( - - - - - {t('dashboard.noResults')} - - - {t('dashboard.tryDifferent')} - - - )} - /> - ) : ( - - {t('dashboard.searchPlaceholder')} - - )} - - ), + setSearchValue(e.nativeEvent.text), + }, }} - backVisible={false} /> + {searchValue ? ( + { + assertIsString(item); + if (!item.startsWith(DASHBOARD_GAP_PREFIX) && arrayIncludes(TILE_NAMES, item)) { + const Component = tileInfo[item].component; + return ( + setSearchValue('')} + > + + + ); + } + return null; + }} + ListHeaderComponent={() => + filteredTiles.length > 0 ? ( + + {filteredTiles.length}{' '} + {filteredTiles.length === 1 + ? appConfig.dashboard.strings.resultSingular + : appConfig.dashboard.strings.resultPlural} + + ) : null + } + ListEmptyComponent={() => ( + + + + + {t('dashboard.noResults')} + + + {t('dashboard.tryDifferent')} + + + )} + /> + ) : null} ( + + + + + + ), } as const; // Generate display data based on user information @@ -114,17 +121,7 @@ function Profile() { <> - ( - - - - - )} - /> - + - } + , + }} /> - + diff --git a/apps/expo/app/(app)/messages/conversations.android.tsx b/apps/expo/app/(app)/messages/conversations.android.tsx index 742d7bfa52..45508379e1 100644 --- a/apps/expo/app/(app)/messages/conversations.android.tsx +++ b/apps/expo/app/(app)/messages/conversations.android.tsx @@ -1,6 +1,5 @@ import { assertDefined } from '@packrat/guards'; import { - AdaptiveSearchHeader, Avatar, AvatarFallback, Button, @@ -20,7 +19,7 @@ import { Icon } from 'expo-app/components/Icon'; import { cn } from 'expo-app/lib/cn'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import * as Haptics from 'expo-haptics'; -import { router } from 'expo-router'; +import { router, Stack } from 'expo-router'; import * as React from 'react'; import { Dimensions, Platform, Pressable, ScrollView, View, type ViewStyle } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; @@ -59,16 +58,18 @@ export default function ConversationsAndroidScreen() { return ( <> - + , -}; - function SearchBarContent() { const { colors } = useColorScheme(); return ( diff --git a/apps/expo/app/(app)/messages/conversations.tsx b/apps/expo/app/(app)/messages/conversations.tsx index f554b6af61..99bfd519f8 100644 --- a/apps/expo/app/(app)/messages/conversations.tsx +++ b/apps/expo/app/(app)/messages/conversations.tsx @@ -8,7 +8,6 @@ import { createContextItem, createDropdownItem, DropdownMenu, - LargeTitleHeader, List, ListItem, type ListRenderItemInfo, @@ -19,7 +18,7 @@ import { Icon } from 'expo-app/components/Icon'; import { cn } from 'expo-app/lib/cn'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import * as Haptics from 'expo-haptics'; -import { router } from 'expo-router'; +import { router, Stack } from 'expo-router'; import * as React from 'react'; import { Dimensions, @@ -45,7 +44,6 @@ import Animated, { } from 'react-native-reanimated'; export default function ConversationsIosScreen() { - const { colors, isDarkColorScheme } = useColorScheme(); const [isSelecting, setIsSelecting] = React.useState(false); const isSelectingDerived = useDerivedValue(() => isSelecting); const [selectedMessages, setSelectedMessages] = React.useState([]); @@ -80,13 +78,20 @@ export default function ConversationsIosScreen() { return ( <> - } - rightView={rightView} - backgroundColor={isDarkColorScheme ? colors.background : colors.card} - searchBar={SEARCH_BAR} + ( + + ), + headerRight: rightView, + headerSearchBarOptions: { + hideWhenScrolling: true, + }, + }} /> + - ), -}; + ); +} const CONTEXT_MENU_ITEMS = [ createContextItem({ diff --git a/apps/expo/app/(app)/pack-categories/[id].tsx b/apps/expo/app/(app)/pack-categories/[id].tsx index 4f8f97b771..8ee9f33127 100644 --- a/apps/expo/app/(app)/pack-categories/[id].tsx +++ b/apps/expo/app/(app)/pack-categories/[id].tsx @@ -1,11 +1,11 @@ -import { LargeTitleHeader, Text } from '@packrat/ui/nativewindui'; +import { Text } from '@packrat/ui/nativewindui'; import { Icon, type MaterialIconName } from 'expo-app/components/Icon'; import { userStore } from 'expo-app/features/auth/store'; import { usePackDetailsFromStore } from 'expo-app/features/packs/hooks/usePackDetailsFromStore'; import { computeCategorySummaries } from 'expo-app/features/packs/utils'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; -import { useLocalSearchParams } from 'expo-router'; +import { Stack, useLocalSearchParams } from 'expo-router'; import { ScrollView, View } from 'react-native'; function CategoryCard({ @@ -63,7 +63,7 @@ export default function PackCategoriesScreen() { return ( <> - + {categories.length ? ( diff --git a/apps/expo/app/(app)/pack-stats/[id].tsx b/apps/expo/app/(app)/pack-stats/[id].tsx index 834927ee70..d432fa1f37 100644 --- a/apps/expo/app/(app)/pack-stats/[id].tsx +++ b/apps/expo/app/(app)/pack-stats/[id].tsx @@ -1,11 +1,11 @@ -import { Button, LargeTitleHeader, Text } from '@packrat/ui/nativewindui'; +import { Button, Text } from '@packrat/ui/nativewindui'; import { featureFlags } from 'expo-app/config'; import { userStore } from 'expo-app/features/auth/store'; import { usePackDetailsFromStore } from 'expo-app/features/packs/hooks/usePackDetailsFromStore'; import { usePackWeightHistory } from 'expo-app/features/packs/hooks/usePackWeightHistory'; import { computeCategorySummaries } from 'expo-app/features/packs/utils'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; -import { useLocalSearchParams, useRouter } from 'expo-router'; +import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; import { ScrollView, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -33,7 +33,7 @@ export default function PackStatsScreen() { return ( - + {/* Weight History Section */} diff --git a/apps/expo/app/(app)/recent-packs.tsx b/apps/expo/app/(app)/recent-packs.tsx index f58da2a2fa..b2377ed2c6 100644 --- a/apps/expo/app/(app)/recent-packs.tsx +++ b/apps/expo/app/(app)/recent-packs.tsx @@ -1,10 +1,11 @@ -import { LargeTitleHeader, Text } from '@packrat/ui/nativewindui'; +import { Text } from '@packrat/ui/nativewindui'; import { Icon } from 'expo-app/components/Icon'; import type { Pack } from 'expo-app/features/packs'; import { useRecentPacks } from 'expo-app/features/packs/hooks/useRecentPacks'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { getRelativeTime } from 'expo-app/lib/utils/getRelativeTime'; +import { Stack } from 'expo-router'; import { Image, Platform, ScrollView, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -65,7 +66,7 @@ export default function RecentPacksScreen() { return ( - + {recentPacks.length ? ( diff --git a/apps/expo/app/(app)/season-suggestions-results.tsx b/apps/expo/app/(app)/season-suggestions-results.tsx index 2101e240ac..86906c1376 100644 --- a/apps/expo/app/(app)/season-suggestions-results.tsx +++ b/apps/expo/app/(app)/season-suggestions-results.tsx @@ -1,4 +1,4 @@ -import { LargeTitleHeader, Text, useColorScheme } from '@packrat/ui/nativewindui'; +import { Text } from '@packrat/ui/nativewindui'; import * as Burnt from 'burnt'; import { Icon } from 'expo-app/components/Icon'; import { PackItemImage } from 'expo-app/features/packs/components/PackItemImage'; @@ -8,9 +8,10 @@ import { SeasonSuggestionsError, useSeasonSuggestions, } from 'expo-app/features/packs/hooks/useSeasonSuggestions'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { LinearGradient } from 'expo-linear-gradient'; -import { useLocalSearchParams, useRouter } from 'expo-router'; +import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; import { useEffect, useRef, useState } from 'react'; import { ScrollView, @@ -376,7 +377,7 @@ export default function SeasonSuggestionsResultsScreen() { return ( <> - + diff --git a/apps/expo/app/(app)/season-suggestions.tsx b/apps/expo/app/(app)/season-suggestions.tsx index f109b5f419..9d7cb0c729 100644 --- a/apps/expo/app/(app)/season-suggestions.tsx +++ b/apps/expo/app/(app)/season-suggestions.tsx @@ -1,6 +1,6 @@ import type { BottomSheetModal } from '@gorhom/bottom-sheet'; import { assertDefined } from '@packrat/guards'; -import { Button, LargeTitleHeader, Text } from '@packrat/ui/nativewindui'; +import { Button, Text } from '@packrat/ui/nativewindui'; import * as Sentry from '@sentry/react-native'; import { Icon } from 'expo-app/components/Icon'; import { LocationSearchSheet } from 'expo-app/features/packs/components/LocationSearchSheet'; @@ -8,7 +8,7 @@ import { LocationSourceSheet } from 'expo-app/features/packs/components/Location import { useBottomSheetAction } from 'expo-app/lib/hooks/useBottomSheetAction'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import * as Location from 'expo-location'; -import { useRouter } from 'expo-router'; +import { Stack, useRouter } from 'expo-router'; import { useRef, useState } from 'react'; import { ActivityIndicator, Alert, Linking, Platform, ScrollView, View } from 'react-native'; @@ -112,7 +112,7 @@ export default function SeasonSuggestionsScreen() { return ( <> - + diff --git a/apps/expo/app/(app)/shared-packs.tsx b/apps/expo/app/(app)/shared-packs.tsx index 76ab6cf6ed..26d8510662 100644 --- a/apps/expo/app/(app)/shared-packs.tsx +++ b/apps/expo/app/(app)/shared-packs.tsx @@ -2,9 +2,9 @@ import { Avatar, AvatarFallback, AvatarImage, - LargeTitleHeader, Text, } from '@packrat/ui/nativewindui'; +import { Stack } from 'expo-router'; import { cn } from 'expo-app/lib/cn'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { ScrollView, View } from 'react-native'; @@ -181,7 +181,7 @@ export default function SharedPacksScreen() { const { t } = useTranslation(); return ( - + diff --git a/apps/expo/app/(app)/shopping-list.tsx b/apps/expo/app/(app)/shopping-list.tsx index 931afdd7e4..d5b7d85cc5 100644 --- a/apps/expo/app/(app)/shopping-list.tsx +++ b/apps/expo/app/(app)/shopping-list.tsx @@ -1,11 +1,12 @@ 'use client'; -import { LargeTitleHeader, Text } from '@packrat/ui/nativewindui'; +import { Text } from '@packrat/ui/nativewindui'; import { Icon } from 'expo-app/components/Icon'; import { cn } from 'expo-app/lib/cn'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import type { TranslationKeys } from 'expo-app/lib/i18n/types'; +import { Stack } from 'expo-router'; import { useState } from 'react'; import { Pressable, ScrollView, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -170,7 +171,7 @@ export default function ShoppingListScreen() { return ( - + diff --git a/apps/expo/app/(app)/trail-conditions.tsx b/apps/expo/app/(app)/trail-conditions.tsx index 984a7cf2f3..e5110c901d 100644 --- a/apps/expo/app/(app)/trail-conditions.tsx +++ b/apps/expo/app/(app)/trail-conditions.tsx @@ -1,4 +1,4 @@ -import { ActivityIndicator, LargeTitleHeader, Text } from '@packrat/ui/nativewindui'; +import { ActivityIndicator, Text } from '@packrat/ui/nativewindui'; import { featureFlags } from 'expo-app/config'; import { SubmitConditionReportForm } from 'expo-app/features/trail-conditions/components/SubmitConditionReportForm'; import { TrailConditionReportCard } from 'expo-app/features/trail-conditions/components/TrailConditionReportCard'; @@ -6,6 +6,7 @@ import { useTrailConditionReports } from 'expo-app/features/trail-conditions/hoo import type { TrailConditionReport, TrailSurface } from 'expo-app/features/trail-conditions/types'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { useMemo, useState } from 'react'; +import { Stack } from 'expo-router'; import { FlatList, Modal, Pressable, ScrollView, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -160,20 +161,23 @@ export default function TrailConditionsScreen() { return ( - ( - setShowSubmitForm(true)} - className="mr-2 rounded-full bg-primary px-3 py-1.5" - accessibilityLabel={t('trailConditions.reportConditionsTitle')} - accessibilityRole="button" - > - - {t('trailConditions.reportButton')} - - - )} + ( + setShowSubmitForm(true)} + className="mr-2 rounded-full bg-primary px-3 py-1.5" + accessibilityLabel={t('trailConditions.reportConditionsTitle')} + accessibilityRole="button" + > + + {t('trailConditions.reportButton')} + + + ), + }} /> diff --git a/apps/expo/app/(app)/weather-alert-preferences.tsx b/apps/expo/app/(app)/weather-alert-preferences.tsx index 1f58119699..4ae7dbd409 100644 --- a/apps/expo/app/(app)/weather-alert-preferences.tsx +++ b/apps/expo/app/(app)/weather-alert-preferences.tsx @@ -1,14 +1,8 @@ -import { - Form, - FormItem, - FormSection, - LargeTitleHeader, - Text, - Toggle, -} from '@packrat/ui/nativewindui'; +import { Form, FormItem, FormSection, Text, Toggle } from '@packrat/ui/nativewindui'; import { Icon } from 'expo-app/components/Icon'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; +import { Stack } from 'expo-router'; import * as React from 'react'; import { ScrollView, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -71,7 +65,7 @@ export default function WeatherAlertPreferencesScreen() { return ( <> - + - + - + { - if (Platform.OS === 'android') { - if (!children) { - return null; - } else { - return children; - } - } - - return ( - - {children} - {!children && } - - ); -}; diff --git a/apps/expo/components/LargeTitleHeaderSearchContentContainer.tsx b/apps/expo/components/LargeTitleHeaderSearchContentContainer.tsx deleted file mode 100644 index f3960ac31f..0000000000 --- a/apps/expo/components/LargeTitleHeaderSearchContentContainer.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useColorScheme } from '@packrat/ui/nativewindui'; -import { Platform, SafeAreaView, View } from 'react-native'; - -export const LargeTitleHeaderSearchContentContainer = ({ - children, -}: { - children: React.ReactNode; -}) => { - const { colors } = useColorScheme(); - - const Container = Platform.OS === 'ios' ? SafeAreaView : View; - - return {children}; -}; diff --git a/apps/expo/components/Markdown.tsx b/apps/expo/components/Markdown.tsx index af0e8bc9e4..ca91058191 100644 --- a/apps/expo/components/Markdown.tsx +++ b/apps/expo/components/Markdown.tsx @@ -1,4 +1,4 @@ -import { useColorScheme } from '@packrat/ui/nativewindui'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import RNMarkdown from '@ronradtke/react-native-markdown-display'; export function Markdown({ children }: { children: string }) { diff --git a/apps/expo/components/SearchInput.tsx b/apps/expo/components/SearchInput.tsx index 37fded10c8..6893afa5dc 100644 --- a/apps/expo/components/SearchInput.tsx +++ b/apps/expo/components/SearchInput.tsx @@ -1,18 +1,19 @@ import { assertPresent } from '@packrat/guards'; -import { SearchInput as NativeWindUISearchInput } from '@packrat/ui/nativewindui'; import { useKeyboardHideBlur } from 'expo-app/lib/hooks/useKeyboardHideBlur'; import { asNonNullableRef } from 'expo-app/lib/utils/asNonNullableRef'; import { forwardRef, useImperativeHandle, useRef } from 'react'; +import type * as React from 'react'; +import { TextInput } from 'react-native'; /** * Enhanced SearchInput component that automatically handles keyboard hide blur fix. - * Drop-in replacement for NativeWindUI's SearchInput with built-in Android keyboard behavior fix. + * Drop-in replacement for a TextInput with built-in Android keyboard behavior fix. */ export const SearchInput = forwardRef< - React.ComponentRef, - React.ComponentProps + React.ComponentRef, + React.ComponentPropsWithoutRef >((props, ref) => { - const searchInputRef = useRef>(null); + const searchInputRef = useRef>(null); // Apply keyboard hide blur fix useKeyboardHideBlur({ textInputRef: asNonNullableRef(searchInputRef) }); @@ -23,7 +24,7 @@ export const SearchInput = forwardRef< return searchInputRef.current; }, []); - return ; + return ; }); SearchInput.displayName = 'SearchInput'; diff --git a/apps/expo/components/initial/ExpandableText.tsx b/apps/expo/components/initial/ExpandableText.tsx index bc567829a8..ea742a3e1d 100644 --- a/apps/expo/components/initial/ExpandableText.tsx +++ b/apps/expo/components/initial/ExpandableText.tsx @@ -1,4 +1,4 @@ -import { cn } from '@packrat/ui/nativewindui'; +import { cn } from 'expo-app/lib/cn'; import { useState } from 'react'; import { Text, TouchableOpacity, View } from 'react-native'; diff --git a/apps/expo/features/ai-packs/screens/AIPacksScreen.tsx b/apps/expo/features/ai-packs/screens/AIPacksScreen.tsx index ba14f45d2b..e047e655a2 100644 --- a/apps/expo/features/ai-packs/screens/AIPacksScreen.tsx +++ b/apps/expo/features/ai-packs/screens/AIPacksScreen.tsx @@ -3,7 +3,6 @@ import { Alert, type AlertMethods, Button, - LargeTitleHeader, Text, } from '@packrat/ui/nativewindui'; import * as Sentry from '@sentry/react-native'; @@ -13,7 +12,7 @@ import { TextInput } from 'expo-app/components/TextInput'; import { PackCard } from 'expo-app/features/packs/components/PackCard'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; -import { useRouter } from 'expo-router'; +import { Stack, useRouter } from 'expo-router'; import { useRef, useState } from 'react'; import { Modal, Platform, ScrollView, TouchableOpacity, View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -92,7 +91,7 @@ export function AIPacksScreen() { return ( - + - + (null); const isSearching = searchValue.trim().length > 0; const trimmedQuery = debouncedSearchValue.trim(); @@ -108,7 +103,6 @@ function CatalogItemsScreen() { return ( <> - - setSearchValue(e.nativeEvent.text), placeholder: t('catalog.searchPlaceholder'), - content: ( - - {isSearching ? ( - isVectorLoading || !isQueryReady ? ( - - - - ) : ( - - - {searchResults.length > 0 && ( - - {searchResults.length} {t('catalog.results')} - - )} - + }, + }} + /> + {isSearching ? ( + isVectorLoading || !isQueryReady ? ( + + + + ) : ( + + + {searchResults.length > 0 && ( + + {searchResults.length} {t('catalog.results')} + + )} + - {searchResults.map((item: CatalogItem) => ( - - handleItemPress(item)} /> - - ))} + {searchResults.map((item: CatalogItem) => ( + + handleItemPress(item)} /> + + ))} - {searchResults.length === 0 && ( - - {vectorError ? ( - <> - - - - - {t('catalog.searchError')} - - - {t('catalog.unableToSearch')} - - - ) : ( - <> - - - - - {t('catalog.noResults')} - - - {t('catalog.tryAdjustingFilters')} - - - )} - - )} - - ) + {searchResults.length === 0 && ( + + {vectorError ? ( + <> + + + + + {t('catalog.searchError')} + + + {t('catalog.unableToSearch')} + + ) : ( - - {t('catalog.searchCatalog')} - + <> + + + + + {t('catalog.noResults')} + + + {t('catalog.tryAdjustingFilters')} + + )} - - ), - } as React.ComponentProps['searchBar'] - } - /> + + )} + + ) + ) : null} { return ( - ( - - - - )} + ( + + + + ), + }} /> {isLoading ? ( diff --git a/apps/expo/features/guides/screens/GuidesListScreen.tsx b/apps/expo/features/guides/screens/GuidesListScreen.tsx index 9322bd8d08..9ef5dcab97 100644 --- a/apps/expo/features/guides/screens/GuidesListScreen.tsx +++ b/apps/expo/features/guides/screens/GuidesListScreen.tsx @@ -1,12 +1,9 @@ -import { LargeTitleHeader, type LargeTitleSearchBarMethods, Text } from '@packrat/ui/nativewindui'; +import { Text } from '@packrat/ui/nativewindui'; import { CategoriesFilter } from 'expo-app/components/CategoriesFilter'; -import { LargeTitleHeaderOverlapFixIOS } from 'expo-app/components/LargeTitleHeaderOverlapFixIOS'; -import { LargeTitleHeaderSearchContentContainer } from 'expo-app/components/LargeTitleHeaderSearchContentContainer'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; -import { asNonNullableRef } from 'expo-app/lib/utils/asNonNullableRef'; -import { useRouter } from 'expo-router'; -import { useCallback, useRef, useState } from 'react'; +import { Stack, useRouter } from 'expo-router'; +import { useCallback, useState } from 'react'; import { ActivityIndicator, FlatList, RefreshControl, View } from 'react-native'; import { GuideCard } from '../components/GuideCard'; import { useGuideCategories, useGuides, useSearchGuides } from '../hooks'; @@ -20,7 +17,6 @@ export const GuidesListScreen = () => { const [selectedCategory, setSelectedCategory] = useState(() => t('guides.all')); const [isManualRefresh, setIsManualRefresh] = useState(false); - const searchBarRef = useRef(null); const { data: categories, @@ -184,7 +180,6 @@ export const GuidesListScreen = () => { return ( <> - { return ( <> - - {renderSearchContent()} - - ), + handleSearch(e.nativeEvent.text), + placeholder: t('guides.searchPlaceholder'), + }, }} /> + {renderSearchContent()} (null); - const searchBarRef = useRef(null); // Filter options with translations const filterOptions: FilterOption[] = [ @@ -186,27 +182,23 @@ export function PackTemplateListScreen() { return ( - - {renderSearchContent()} - + ( + + templateOptionsRef.current?.present()} /> + ), + headerSearchBarOptions: { + hideWhenScrolling: false, + onChangeText: (e) => setSearchValue(e.nativeEvent.text), + placeholder: t('packTemplates.searchPlaceholder'), + }, }} - rightView={() => ( - - templateOptionsRef.current?.present()} /> - - )} /> + {renderSearchContent()} (null); const { colors } = useColorScheme(); @@ -193,36 +183,27 @@ export function PackListScreen() { return ( - - - {searchValue ? ( - - ) : ( - - {t('packs.searchPacks')} - - )} - - ), - }} - rightView={() => } + , + headerSearchBarOptions: { + onChangeText: (e) => setSearchValue(e.nativeEvent.text), + }, + }} + /> + {searchValue ? ( + + ) : null} - pack.id} stickyHeaderIndices={[0]} @@ -305,7 +286,6 @@ export function PackListScreen() { ListFooterComponent={} contentContainerStyle={{ flexGrow: 1 }} /> - ); } diff --git a/apps/expo/features/packs/utils/getPackDetailOptions.tsx b/apps/expo/features/packs/utils/getPackDetailOptions.tsx index 8da15b9269..89a9c7ce2a 100644 --- a/apps/expo/features/packs/utils/getPackDetailOptions.tsx +++ b/apps/expo/features/packs/utils/getPackDetailOptions.tsx @@ -1,4 +1,5 @@ -import { Button, useColorScheme, useSheetRef } from '@packrat/ui/nativewindui'; +import { Button, useSheetRef } from '@packrat/ui/nativewindui'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { appAlert } from 'expo-app/app/_layout'; import { Icon } from 'expo-app/components/Icon'; import { t } from 'expo-app/lib/i18n'; diff --git a/apps/expo/features/packs/utils/getPackItemDetailOptions.tsx b/apps/expo/features/packs/utils/getPackItemDetailOptions.tsx index 6f166f2862..7d8820f4e5 100644 --- a/apps/expo/features/packs/utils/getPackItemDetailOptions.tsx +++ b/apps/expo/features/packs/utils/getPackItemDetailOptions.tsx @@ -1,5 +1,6 @@ import { assertDefined } from '@packrat/guards'; -import { Alert, Button, useColorScheme } from '@packrat/ui/nativewindui'; +import { Alert, Button } from '@packrat/ui/nativewindui'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { Icon } from 'expo-app/components/Icon'; import { t } from 'expo-app/lib/i18n'; import { useRouter } from 'expo-router'; diff --git a/apps/expo/features/trips/components/UpcomingTripsTile.tsx b/apps/expo/features/trips/components/UpcomingTripsTile.tsx index 00de15c86e..c1faec11e0 100644 --- a/apps/expo/features/trips/components/UpcomingTripsTile.tsx +++ b/apps/expo/features/trips/components/UpcomingTripsTile.tsx @@ -1,5 +1,6 @@ import type { AlertMethods } from '@packrat/ui/nativewindui'; -import { Alert, ListItem, Text, useColorScheme } from '@packrat/ui/nativewindui'; +import { Alert, ListItem, Text } from '@packrat/ui/nativewindui'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { Icon } from 'expo-app/components/Icon'; import { featureFlags } from 'expo-app/config'; import { useTrips } from 'expo-app/features/trips/hooks'; diff --git a/apps/expo/features/trips/screens/TripListScreen.tsx b/apps/expo/features/trips/screens/TripListScreen.tsx index 669d8ac6e3..8f079e173a 100644 --- a/apps/expo/features/trips/screens/TripListScreen.tsx +++ b/apps/expo/features/trips/screens/TripListScreen.tsx @@ -1,13 +1,11 @@ -import { LargeTitleHeader, type LargeTitleSearchBarMethods } from '@packrat/ui/nativewindui'; + import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; import { Icon } from 'expo-app/components/Icon'; -import { LargeTitleHeaderSearchContentContainer } from 'expo-app/components/LargeTitleHeaderSearchContentContainer'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { testIds } from 'expo-app/lib/testIds'; -import { asNonNullableRef } from 'expo-app/lib/utils/asNonNullableRef'; -import { Link, useRouter } from 'expo-router'; -import { useCallback, useMemo, useRef, useState } from 'react'; +import { Link, Stack, useRouter } from 'expo-router'; +import { useCallback, useMemo, useState } from 'react'; import { FlatList, Pressable, ScrollView, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { TripCard } from '../components/TripCard'; @@ -57,7 +55,6 @@ export function TripsListScreen() { const router = useRouter(); const { t } = useTranslation(); const trips = useTrips(); - const searchBarRef = useRef(null); const [searchValue, setSearchValue] = useState(''); const filteredTrips = useMemo(() => { @@ -155,22 +152,20 @@ export function TripsListScreen() { return ( - - {renderSearchContent()} - - ), + , + headerSearchBarOptions: { + hideWhenScrolling: false, + onChangeText: (e) => setSearchValue(e.nativeEvent.text), + placeholder: t('trips.searchPlaceholder'), + }, }} - rightView={() => } /> + {renderSearchContent()} diff --git a/apps/expo/features/weather/screens/LocationsScreen.tsx b/apps/expo/features/weather/screens/LocationsScreen.tsx index 0e3584c5b4..7b70247039 100644 --- a/apps/expo/features/weather/screens/LocationsScreen.tsx +++ b/apps/expo/features/weather/screens/LocationsScreen.tsx @@ -1,11 +1,10 @@ -import { Button, LargeTitleHeader, Text } from '@packrat/ui/nativewindui'; +import { Button, Text } from '@packrat/ui/nativewindui'; import { Icon } from 'expo-app/components/Icon'; -import { LargeTitleHeaderOverlapFixIOS } from 'expo-app/components/LargeTitleHeaderOverlapFixIOS'; import { SearchInput } from 'expo-app/components/SearchInput'; import { withAuthWall } from 'expo-app/features/auth/hocs'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; -import { router, useNavigation } from 'expo-router'; +import { Stack, router, useNavigation } from 'expo-router'; import { useAtom } from 'jotai'; import { useCallback, useEffect, useRef, useState } from 'react'; import { @@ -17,6 +16,7 @@ import { ScrollView, TouchableOpacity, View, + TextInput, } from 'react-native'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -35,7 +35,7 @@ function LocationsScreen() { const { setActiveLocation } = useActiveLocation(); const { isRefreshing, refreshAllLocations } = useLocationRefresh(); const [isSearchFocused, setIsSearchFocused] = useState(false); - const searchInputRef = useRef(null); + const searchInputRef = useRef(null); const { removeLocation } = useLocations(); // Determine if we're loading @@ -118,15 +118,17 @@ function LocationsScreen() { return ( - - ( + ( - )} - /> + ), + }} + /> setIsSearchFocused(true)} onBlur={() => { // Only unfocus if search is empty @@ -246,7 +247,6 @@ function LocationsScreen() { )} )} - ); } diff --git a/apps/expo/features/wildlife/screens/WildlifeScreen.tsx b/apps/expo/features/wildlife/screens/WildlifeScreen.tsx index c48d234e17..c6cb81abf2 100644 --- a/apps/expo/features/wildlife/screens/WildlifeScreen.tsx +++ b/apps/expo/features/wildlife/screens/WildlifeScreen.tsx @@ -1,8 +1,8 @@ -import { LargeTitleHeader, Text } from '@packrat/ui/nativewindui'; +import { Text } from '@packrat/ui/nativewindui'; import { Icon } from 'expo-app/components/Icon'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; -import { useRouter } from 'expo-router'; +import { Stack, useRouter } from 'expo-router'; import { ActivityIndicator, FlatList, Pressable, SafeAreaView, View } from 'react-native'; import { useWildlifeHistory } from '../hooks/useWildlifeHistory'; import type { WildlifeIdentification } from '../types'; @@ -65,7 +65,7 @@ export function WildlifeScreen() { return ( - + {/* Identify FAB */} From 7766c6724b2b9cfc52b1a7c40e0c8d6e04f0387d Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Sun, 14 Jun 2026 12:38:25 +0100 Subject: [PATCH 02/17] chore(migration): add tracker, docs, script, biome fixes, and config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - packages/ui/nativewindui/index.ts: mark Phase 1 + 2 as done, preserve Phase 3–5 tracking structure - biome.json: add organizeImports:off override for the adapter tracker file so Biome doesn't collapse per-phase exports - scripts/lint/nativewindui-migration.ts: CI script counting remaining adapter exports per phase + bypass-import detection - package.json: add check:migration script entry - docs/migrations/nativewindui-to-expo-ui.md: full migration plan with phase breakdown, replacement map, and PR checklist - docs/ideation/2026-06-13-nativewindui-to-expo-ui-ideation.md: ranked approach ideation doc - apps/expo/app/_layout.web.tsx: fix direct @packrat-ai/nativewindui import → adapter - remaining files: Biome import-order corrections applied by bun lint --- apps/expo/app/(app)/(tabs)/profile/index.tsx | 2 - apps/expo/app/(app)/current-pack/[id].tsx | 7 +- apps/expo/app/(app)/pack-stats/[id].tsx | 4 +- apps/expo/app/(app)/shared-packs.tsx | 9 +- apps/expo/app/(app)/trail-conditions.tsx | 2 +- .../app/(app)/weather-alert-preferences.tsx | 4 +- apps/expo/app/_layout.web.tsx | 2 +- apps/expo/components/ErrorState.tsx | 2 +- apps/expo/components/Markdown.tsx | 2 +- apps/expo/components/SearchInput.tsx | 2 +- .../features/ai/components/ChatBubble.tsx | 2 +- .../ai/components/GuidesRAGGenerativeUI.tsx | 2 +- .../ai/components/LocationContext.tsx | 2 +- .../ai/components/WeatherGenerativeUI.tsx | 2 +- .../ai/screens/ReportedContentScreen.tsx | 3 +- .../catalog/components/CatalogItemImage.tsx | 2 +- .../guides/screens/GuidesListScreen.tsx | 1 - .../components/AddPackTemplateItemActions.tsx | 2 +- .../components/PackTemplateItemImage.tsx | 2 +- .../screens/PackTemplateListScreen.tsx | 1 - .../utils/getPackTemplateDetailOptions.tsx | 2 +- .../getPackTemplateItemDetailOptions.tsx | 2 +- .../packs/components/AddPackItemActions.tsx | 2 +- .../packs/components/GapAnalysisModal.tsx | 2 +- .../packs/components/PackItemImage.tsx | 2 +- .../packs/screens/PackItemDetailScreen.tsx | 2 +- .../features/packs/screens/PackListScreen.tsx | 157 +++++++------ .../packs/utils/getPackDetailOptions.tsx | 2 +- .../packs/utils/getPackItemDetailOptions.tsx | 2 +- .../trips/components/UpcomingTripsTile.tsx | 2 +- .../features/trips/screens/TripListScreen.tsx | 1 - .../trips/utils/getTripDetailOptions.tsx | 2 +- .../weather/screens/LocationsScreen.tsx | 218 +++++++++--------- .../wildlife/screens/WildlifeScreen.tsx | 8 +- biome.json | 10 + ...-06-13-nativewindui-to-expo-ui-ideation.md | 165 +++++++++++++ docs/migrations/nativewindui-to-expo-ui.md | 162 +++++++++++++ package.json | 1 + packages/ui/nativewindui/index.ts | 57 ++++- scripts/lint/nativewindui-migration.ts | 99 ++++++++ 40 files changed, 719 insertions(+), 234 deletions(-) create mode 100644 docs/ideation/2026-06-13-nativewindui-to-expo-ui-ideation.md create mode 100644 docs/migrations/nativewindui-to-expo-ui.md create mode 100644 scripts/lint/nativewindui-migration.ts diff --git a/apps/expo/app/(app)/(tabs)/profile/index.tsx b/apps/expo/app/(app)/(tabs)/profile/index.tsx index bdef81646a..c7769d0770 100644 --- a/apps/expo/app/(app)/(tabs)/profile/index.tsx +++ b/apps/expo/app/(app)/(tabs)/profile/index.tsx @@ -121,8 +121,6 @@ function Profile() { <> - - - + {/* Weight History Section */} diff --git a/apps/expo/app/(app)/shared-packs.tsx b/apps/expo/app/(app)/shared-packs.tsx index 26d8510662..2c90dd1dac 100644 --- a/apps/expo/app/(app)/shared-packs.tsx +++ b/apps/expo/app/(app)/shared-packs.tsx @@ -1,12 +1,7 @@ -import { - Avatar, - AvatarFallback, - AvatarImage, - Text, -} from '@packrat/ui/nativewindui'; -import { Stack } from 'expo-router'; +import { Avatar, AvatarFallback, AvatarImage, Text } from '@packrat/ui/nativewindui'; import { cn } from 'expo-app/lib/cn'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; +import { Stack } from 'expo-router'; import { ScrollView, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; diff --git a/apps/expo/app/(app)/trail-conditions.tsx b/apps/expo/app/(app)/trail-conditions.tsx index e5110c901d..c9025d9d1c 100644 --- a/apps/expo/app/(app)/trail-conditions.tsx +++ b/apps/expo/app/(app)/trail-conditions.tsx @@ -5,8 +5,8 @@ import { TrailConditionReportCard } from 'expo-app/features/trail-conditions/com import { useTrailConditionReports } from 'expo-app/features/trail-conditions/hooks/useTrailConditionReports'; import type { TrailConditionReport, TrailSurface } from 'expo-app/features/trail-conditions/types'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; -import { useMemo, useState } from 'react'; import { Stack } from 'expo-router'; +import { useMemo, useState } from 'react'; import { FlatList, Modal, Pressable, ScrollView, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; diff --git a/apps/expo/app/(app)/weather-alert-preferences.tsx b/apps/expo/app/(app)/weather-alert-preferences.tsx index 4ae7dbd409..4ca305da42 100644 --- a/apps/expo/app/(app)/weather-alert-preferences.tsx +++ b/apps/expo/app/(app)/weather-alert-preferences.tsx @@ -65,7 +65,9 @@ export default function WeatherAlertPreferencesScreen() { return ( <> - + { diff --git a/apps/expo/features/guides/screens/GuidesListScreen.tsx b/apps/expo/features/guides/screens/GuidesListScreen.tsx index 9ef5dcab97..6000082fa9 100644 --- a/apps/expo/features/guides/screens/GuidesListScreen.tsx +++ b/apps/expo/features/guides/screens/GuidesListScreen.tsx @@ -17,7 +17,6 @@ export const GuidesListScreen = () => { const [selectedCategory, setSelectedCategory] = useState(() => t('guides.all')); const [isManualRefresh, setIsManualRefresh] = useState(false); - const { data: categories, error: categoriesError, diff --git a/apps/expo/features/pack-templates/components/AddPackTemplateItemActions.tsx b/apps/expo/features/pack-templates/components/AddPackTemplateItemActions.tsx index 2df6e45f09..dab4845123 100644 --- a/apps/expo/features/pack-templates/components/AddPackTemplateItemActions.tsx +++ b/apps/expo/features/pack-templates/components/AddPackTemplateItemActions.tsx @@ -3,7 +3,6 @@ import type { BottomSheetModal } from '@gorhom/bottom-sheet'; import { BottomSheetView } from '@gorhom/bottom-sheet'; import { isFunction } from '@packrat/guards'; import { Sheet, Text } from '@packrat/ui/nativewindui'; -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import * as Burnt from 'burnt'; import { appAlert } from 'expo-app/app/_layout'; import { Icon } from 'expo-app/components/Icon'; @@ -12,6 +11,7 @@ import { CatalogBrowserModal } from 'expo-app/features/catalog/components'; import { useRecentlyUsedCatalogItems } from 'expo-app/features/catalog/hooks/useRecentlyUsedCatalogItems'; import type { CatalogItem } from 'expo-app/features/catalog/types'; import { useImagePicker } from 'expo-app/features/packs'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { router } from 'expo-router'; import React from 'react'; diff --git a/apps/expo/features/pack-templates/components/PackTemplateItemImage.tsx b/apps/expo/features/pack-templates/components/PackTemplateItemImage.tsx index f9598bb83e..95124fe1e8 100644 --- a/apps/expo/features/pack-templates/components/PackTemplateItemImage.tsx +++ b/apps/expo/features/pack-templates/components/PackTemplateItemImage.tsx @@ -1,7 +1,7 @@ -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { Icon } from 'expo-app/components/Icon'; import { CatalogItemImage } from 'expo-app/features/catalog/components/CatalogItemImage'; import { CachedImage } from 'expo-app/features/packs/components/CachedImage'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { buildImageUrl } from 'expo-app/lib/utils/buildImageUrl'; import { type ImageProps, View } from 'react-native'; import type { PackTemplateItem } from '../types'; diff --git a/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx b/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx index 2d8999a37a..8cb1398b01 100644 --- a/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx +++ b/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx @@ -52,7 +52,6 @@ export function PackTemplateListScreen() { const { t } = useTranslation(); const templateOptionsRef = useRef(null); - // Filter options with translations const filterOptions: FilterOption[] = [ { label: t('packTemplates.all'), value: 'all' }, diff --git a/apps/expo/features/pack-templates/utils/getPackTemplateDetailOptions.tsx b/apps/expo/features/pack-templates/utils/getPackTemplateDetailOptions.tsx index 2ecafca3a7..31e12b8acf 100644 --- a/apps/expo/features/pack-templates/utils/getPackTemplateDetailOptions.tsx +++ b/apps/expo/features/pack-templates/utils/getPackTemplateDetailOptions.tsx @@ -1,6 +1,6 @@ import { Alert, Button, useSheetRef } from '@packrat/ui/nativewindui'; -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { Icon } from 'expo-app/components/Icon'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { t } from 'expo-app/lib/i18n'; import { useRouter } from 'expo-router'; diff --git a/apps/expo/features/pack-templates/utils/getPackTemplateItemDetailOptions.tsx b/apps/expo/features/pack-templates/utils/getPackTemplateItemDetailOptions.tsx index 3262d77545..28ddb17a48 100644 --- a/apps/expo/features/pack-templates/utils/getPackTemplateItemDetailOptions.tsx +++ b/apps/expo/features/pack-templates/utils/getPackTemplateItemDetailOptions.tsx @@ -1,7 +1,7 @@ import { assertDefined } from '@packrat/guards'; import { Alert, Button } from '@packrat/ui/nativewindui'; -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { Icon } from 'expo-app/components/Icon'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { t } from 'expo-app/lib/i18n'; import { useRouter } from 'expo-router'; diff --git a/apps/expo/features/packs/components/AddPackItemActions.tsx b/apps/expo/features/packs/components/AddPackItemActions.tsx index 530c71e5ac..15be921f78 100644 --- a/apps/expo/features/packs/components/AddPackItemActions.tsx +++ b/apps/expo/features/packs/components/AddPackItemActions.tsx @@ -3,12 +3,12 @@ import type { BottomSheetModal } from '@gorhom/bottom-sheet'; import { BottomSheetView } from '@gorhom/bottom-sheet'; import { isFunction } from '@packrat/guards'; import { Sheet, Text } from '@packrat/ui/nativewindui'; -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { Icon } from 'expo-app/components/Icon'; import { isAuthed } from 'expo-app/features/auth/store'; import { CatalogBrowserModal } from 'expo-app/features/catalog/components'; import { useRecentlyUsedCatalogItems } from 'expo-app/features/catalog/hooks/useRecentlyUsedCatalogItems'; import type { CatalogItem } from 'expo-app/features/catalog/types'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { testIds } from 'expo-app/lib/testIds'; import { router } from 'expo-router'; diff --git a/apps/expo/features/packs/components/GapAnalysisModal.tsx b/apps/expo/features/packs/components/GapAnalysisModal.tsx index 960a2f6bb5..1e2dc90e13 100644 --- a/apps/expo/features/packs/components/GapAnalysisModal.tsx +++ b/apps/expo/features/packs/components/GapAnalysisModal.tsx @@ -1,9 +1,9 @@ import { ActivityIndicator, Button, Text } from '@packrat/ui/nativewindui'; -import { cn } from 'expo-app/lib/cn'; import { devSkipAutoAnalyzeAtom } from 'expo-app/atoms/devAtoms'; import { Icon } from 'expo-app/components/Icon'; import { CatalogItemImage } from 'expo-app/features/catalog/components/CatalogItemImage'; import type { CatalogItem } from 'expo-app/features/catalog/types'; +import { cn } from 'expo-app/lib/cn'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { useAtom } from 'jotai'; diff --git a/apps/expo/features/packs/components/PackItemImage.tsx b/apps/expo/features/packs/components/PackItemImage.tsx index d3831b0278..cf256df8de 100644 --- a/apps/expo/features/packs/components/PackItemImage.tsx +++ b/apps/expo/features/packs/components/PackItemImage.tsx @@ -1,7 +1,7 @@ import { isRemoteUrl } from '@packrat/guards'; -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { Icon } from 'expo-app/components/Icon'; import { CatalogItemImage } from 'expo-app/features/catalog/components/CatalogItemImage'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { buildImageUrl } from 'expo-app/lib/utils/buildImageUrl'; import { Image, type ImageProps, View } from 'react-native'; import { usePackItemOwnershipCheck } from '../hooks'; diff --git a/apps/expo/features/packs/screens/PackItemDetailScreen.tsx b/apps/expo/features/packs/screens/PackItemDetailScreen.tsx index da2f161556..02b8c94505 100644 --- a/apps/expo/features/packs/screens/PackItemDetailScreen.tsx +++ b/apps/expo/features/packs/screens/PackItemDetailScreen.tsx @@ -1,9 +1,9 @@ import { isDefined } from '@packrat/guards'; import { ActivityIndicator, Button, Text } from '@packrat/ui/nativewindui'; -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { Icon } from 'expo-app/components/Icon'; import { Chip } from 'expo-app/components/initial/Chip'; import { WeightBadge } from 'expo-app/components/initial/WeightBadge'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { calculateTotalWeight, diff --git a/apps/expo/features/packs/screens/PackListScreen.tsx b/apps/expo/features/packs/screens/PackListScreen.tsx index 12d031be33..e729d21fc7 100644 --- a/apps/expo/features/packs/screens/PackListScreen.tsx +++ b/apps/expo/features/packs/screens/PackListScreen.tsx @@ -64,7 +64,6 @@ export function PackListScreen() { ); const allPacksQuery = useAllPacks(selectedTypeIndex === ALL_PACKS_INDEX); - const { colors } = useColorScheme(); const filterOptions: FilterOption[] = [ @@ -204,88 +203,88 @@ export function PackListScreen() { ) : null} pack.id} - stickyHeaderIndices={[0]} - contentInsetAdjustmentBehavior="automatic" - renderItem={({ item: pack }) => ( - - - - )} - refreshControl={ - selectedTypeIndex === ALL_PACKS_INDEX ? ( - - ) : undefined - } - ListHeaderComponent={ - - {!isAuthenticated && } - {isAuthenticated && ( - - { - setSelectedTypeIndex(index); - }} - /> - - )} - - - {filterOptions.map(renderFilterChip)} - + data={filteredPacks} + keyExtractor={(pack) => pack.id} + stickyHeaderIndices={[0]} + contentInsetAdjustmentBehavior="automatic" + renderItem={({ item: pack }) => ( + + + + )} + refreshControl={ + selectedTypeIndex === ALL_PACKS_INDEX ? ( + + ) : undefined + } + ListHeaderComponent={ + + {!isAuthenticated && } + {isAuthenticated && ( + + { + setSelectedTypeIndex(index); + }} + /> - {selectedTypeIndex === USER_PACKS_INDEX && ( - - - {filteredPacks?.length || 0} {filteredPacks?.length === 1 ? 'pack' : 'packs'} - - - )} + )} + + + {filterOptions.map(renderFilterChip)} + - } - ListEmptyComponent={ - selectedTypeIndex === ALL_PACKS_INDEX ? ( - renderAllPacksEmptyState() - ) : ( - - - - - - {t('packs.noPacksFound')} + {selectedTypeIndex === USER_PACKS_INDEX && ( + + + {filteredPacks?.length || 0} {filteredPacks?.length === 1 ? 'pack' : 'packs'} - - {activeFilter === 'all' - ? "You haven't created or found any public packs yet." - : `You don't have any ${activeFilter} packs.`} - - - - {t('packs.createNewPack')} - - - ) - } - ListFooterComponent={} - contentContainerStyle={{ flexGrow: 1 }} - /> + )} + + } + ListEmptyComponent={ + selectedTypeIndex === ALL_PACKS_INDEX ? ( + renderAllPacksEmptyState() + ) : ( + + + + + + {t('packs.noPacksFound')} + + + {activeFilter === 'all' + ? "You haven't created or found any public packs yet." + : `You don't have any ${activeFilter} packs.`} + + + + {t('packs.createNewPack')} + + + + ) + } + ListFooterComponent={} + contentContainerStyle={{ flexGrow: 1 }} + /> ); } diff --git a/apps/expo/features/packs/utils/getPackDetailOptions.tsx b/apps/expo/features/packs/utils/getPackDetailOptions.tsx index 89a9c7ce2a..8c085dbe23 100644 --- a/apps/expo/features/packs/utils/getPackDetailOptions.tsx +++ b/apps/expo/features/packs/utils/getPackDetailOptions.tsx @@ -1,7 +1,7 @@ import { Button, useSheetRef } from '@packrat/ui/nativewindui'; -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { appAlert } from 'expo-app/app/_layout'; import { Icon } from 'expo-app/components/Icon'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { t } from 'expo-app/lib/i18n'; import { testIds } from 'expo-app/lib/testIds'; import { useRouter } from 'expo-router'; diff --git a/apps/expo/features/packs/utils/getPackItemDetailOptions.tsx b/apps/expo/features/packs/utils/getPackItemDetailOptions.tsx index 7d8820f4e5..2023216e9e 100644 --- a/apps/expo/features/packs/utils/getPackItemDetailOptions.tsx +++ b/apps/expo/features/packs/utils/getPackItemDetailOptions.tsx @@ -1,7 +1,7 @@ import { assertDefined } from '@packrat/guards'; import { Alert, Button } from '@packrat/ui/nativewindui'; -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { Icon } from 'expo-app/components/Icon'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { t } from 'expo-app/lib/i18n'; import { useRouter } from 'expo-router'; import { View } from 'react-native'; diff --git a/apps/expo/features/trips/components/UpcomingTripsTile.tsx b/apps/expo/features/trips/components/UpcomingTripsTile.tsx index c1faec11e0..9f3ed359df 100644 --- a/apps/expo/features/trips/components/UpcomingTripsTile.tsx +++ b/apps/expo/features/trips/components/UpcomingTripsTile.tsx @@ -1,9 +1,9 @@ import type { AlertMethods } from '@packrat/ui/nativewindui'; import { Alert, ListItem, Text } from '@packrat/ui/nativewindui'; -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { Icon } from 'expo-app/components/Icon'; import { featureFlags } from 'expo-app/config'; import { useTrips } from 'expo-app/features/trips/hooks'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { parseLocalDate } from 'expo-app/lib/utils/dateUtils'; import { useRouter } from 'expo-router'; diff --git a/apps/expo/features/trips/screens/TripListScreen.tsx b/apps/expo/features/trips/screens/TripListScreen.tsx index 8f079e173a..08a397038a 100644 --- a/apps/expo/features/trips/screens/TripListScreen.tsx +++ b/apps/expo/features/trips/screens/TripListScreen.tsx @@ -1,4 +1,3 @@ - import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; import { Icon } from 'expo-app/components/Icon'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; diff --git a/apps/expo/features/trips/utils/getTripDetailOptions.tsx b/apps/expo/features/trips/utils/getTripDetailOptions.tsx index 6812803804..f8b965827c 100644 --- a/apps/expo/features/trips/utils/getTripDetailOptions.tsx +++ b/apps/expo/features/trips/utils/getTripDetailOptions.tsx @@ -1,8 +1,8 @@ import { Button } from '@packrat/ui/nativewindui'; -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { appAlert } from 'expo-app/app/_layout'; import { Icon } from 'expo-app/components/Icon'; import { useTripDetailsFromStore } from 'expo-app/features/trips/hooks/useTripDetailsFromStore'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { t } from 'expo-app/lib/i18n'; import { testIds } from 'expo-app/lib/testIds'; diff --git a/apps/expo/features/weather/screens/LocationsScreen.tsx b/apps/expo/features/weather/screens/LocationsScreen.tsx index 7b70247039..ad3074b04b 100644 --- a/apps/expo/features/weather/screens/LocationsScreen.tsx +++ b/apps/expo/features/weather/screens/LocationsScreen.tsx @@ -4,7 +4,7 @@ import { SearchInput } from 'expo-app/components/SearchInput'; import { withAuthWall } from 'expo-app/features/auth/hocs'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; -import { Stack, router, useNavigation } from 'expo-router'; +import { router, Stack, useNavigation } from 'expo-router'; import { useAtom } from 'jotai'; import { useCallback, useEffect, useRef, useState } from 'react'; import { @@ -14,9 +14,9 @@ import { Pressable, RefreshControl, ScrollView, + type TextInput, TouchableOpacity, View, - TextInput, } from 'react-native'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -130,123 +130,123 @@ function LocationsScreen() { }} /> - - setIsSearchFocused(true)} - onBlur={() => { - // Only unfocus if search is empty - if (searchQuery.length === 0) { - setIsSearchFocused(false); - } - }} - /> - - - {showNoSearchResults && ( - - - {t('weather.searchResults')} + + setIsSearchFocused(true)} + onBlur={() => { + // Only unfocus if search is empty + if (searchQuery.length === 0) { + setIsSearchFocused(false); + } + }} + /> + + + {showNoSearchResults && ( + + + {t('weather.searchResults')} + + + + + {t('weather.noLocationsMatch', { query: searchQuery })} - - - - {t('weather.noLocationsMatch', { query: searchQuery })} - - - - {t('weather.clearSearch')} - - - {t('weather.addNewLocation')} - - + + + {t('weather.clearSearch')} + + + {t('weather.addNewLocation')} + - - )} - - {isLoading ? ( - - - {t('weather.loadingWeatherData')} - ) : ( - - } - keyboardShouldPersistTaps="handled" - > - {showLocationsList && ( - <> - {showSearchResults && ( - - - {filteredLocations.length}{' '} - {filteredLocations.length === 1 ? t('weather.result') : t('weather.results')} - - - )} + + )} + {isLoading ? ( + + + {t('weather.loadingWeatherData')} + + ) : ( + + } + keyboardShouldPersistTaps="handled" + > + {showLocationsList && ( + <> + {showSearchResults && ( - - {t('weather.longPressForOptions')} + + {filteredLocations.length}{' '} + {filteredLocations.length === 1 ? t('weather.result') : t('weather.results')} + )} - {filteredLocations.map((location) => ( - handleLocationPress(location.id)} - onSetActive={() => handleSetActive(location.id)} - onRemove={() => handleRemoveLocation(location.id)} - /> - ))} - - )} - - {showEmptyState && ( - - - - {t('weather.noSavedLocations')} - - - {t('weather.noSavedLocationsDesc')} + + + {t('weather.longPressForOptions')} - - )} - - )} + + {filteredLocations.map((location) => ( + handleLocationPress(location.id)} + onSetActive={() => handleSetActive(location.id)} + onRemove={() => handleRemoveLocation(location.id)} + /> + ))} + + )} + + {showEmptyState && ( + + + + {t('weather.noSavedLocations')} + + + {t('weather.noSavedLocationsDesc')} + + + + )} + + )} ); } diff --git a/apps/expo/features/wildlife/screens/WildlifeScreen.tsx b/apps/expo/features/wildlife/screens/WildlifeScreen.tsx index c6cb81abf2..debf6391ef 100644 --- a/apps/expo/features/wildlife/screens/WildlifeScreen.tsx +++ b/apps/expo/features/wildlife/screens/WildlifeScreen.tsx @@ -65,7 +65,13 @@ export function WildlifeScreen() { return ( - + {/* Identify FAB */} diff --git a/biome.json b/biome.json index 5715215a55..7a88839a87 100644 --- a/biome.json +++ b/biome.json @@ -61,6 +61,16 @@ } }, "overrides": [ + { + "includes": ["packages/ui/nativewindui/index.ts"], + "assist": { + "actions": { + "source": { + "organizeImports": "off" + } + } + } + }, { "includes": [ "apps/expo/atoms/atomWith*.ts", diff --git a/docs/ideation/2026-06-13-nativewindui-to-expo-ui-ideation.md b/docs/ideation/2026-06-13-nativewindui-to-expo-ui-ideation.md new file mode 100644 index 0000000000..dd4db936d8 --- /dev/null +++ b/docs/ideation/2026-06-13-nativewindui-to-expo-ui-ideation.md @@ -0,0 +1,165 @@ +--- +date: 2026-06-13 +topic: nativewindui-to-expo-ui-migration +focus: migrate from nativewindui to expo ui +mode: repo-grounded +--- + +# Ideation: nativewindui → Expo UI Migration + +## Grounding Context + +**Codebase shape:** Bun monorepo (3 web + 1 mobile). Mobile app (apps/expo): Expo SDK 56, React Native 0.85, NativeWind v4.2.3. Uses rn-primitives packages (alert-dialog, avatar, checkbox, etc.) and wraps them with `@packrat-ai/nativewindui` v2.2.0. 177 nativewindui imports across 87 files. Team maintains custom TextInput, Button, SearchInput components in `apps/expo/components/`. Recent Expo 56 + React Native 0.85 upgrade (June 2026). + +**Notable patterns:** Feature modules in `apps/expo/features/{name}/` with own components, hooks, screens. Styling via NativeWind (Tailwind for RN) + CSS variable-based color system. State: Jotai (local), React Query (server), Legend State (reactive). Forms: TanStack React Form + Zod. E2E: Maestro with stable testID selectors from `lib/testIds.ts`. Feature flags in `apps/expo/config.ts`. EAS Build with dev/preview/e2e/production profiles + EAS Updates. + +**Pain points:** (1) GitHub Packages token requirement (PACKRAT_NATIVEWIND_UI_GITHUB_TOKEN friction). (2) Type breaking changes (AlertRef → AlertMethods, LargeTitleSearchBarRef → LargeTitleSearchBarMethods) affecting 18+ files per release. (3) Hard to diagnose platform-specific bugs when wrappers hide root causes (Android TextInput keyboard focus issue documented in `docs/solutions/ui-bugs/`). (4) Monolithic nativewindui package (50+ components, but PackRat uses only 15-20). + +**Leverage points:** rn-primitives actively maintained (v1.4.0, March 2026) and covers ~80% of use cases. @expo/ui stable in SDK 56 (same platform PackRat already upgraded to). Web apps (guides, landing) use Shadcn/Radix UI, enabling cross-platform code reuse via rn-primitives foundation. Team already maintains thin wrappers (components/TextInput.tsx shows pattern works). + +**Past learnings:** Institutional experience (Android TextInput bug, documented solution) revealed that wrapper patterns hide root causes. Solution: thin enhancement wrappers (hooks + useImperativeHandle) on top of native APIs. Systematic import migration required. Keep platform-specific wraps minimal. + +**External context:** @expo/ui built on New Architecture (JSI + Fabric). rn-primitives is Radix UI-equivalent for RN (actively maintained, same foundation as web apps). React Native Reusables (copy-own model) gaining traction in ecosystem. NativeBase effectively dead. NativeWind v5 has migration friction (lightningcss issue) — recommend staying on v4 for now, independent of this migration. + +--- + +## Ranked Ideas + +### 1. Monomorphism: Migrate One Component at a Time +**Description:** Pick the highest-impact, lowest-risk component (Text: 27 imports) and migrate all 27 import sites in one PR. Create `apps/expo/components/Text.tsx` wrapping @expo/ui or rn-primitives Text, update all imports, test on device, ship. Repeat for Button (15 imports), then LargeTitleHeader (7), then Refs (Alert, Sheet, ContextMenu). Frontload simple components; ones with ref-forwarding requirements go last. + +**Rationale:** Boring is reliable. No clever abstractions, no generators, no runtime swapping—just straightforward component migration. Each PR becomes an audit trail of what moved and when. Grounded in team practice: `apps/expo/components/TextInput.tsx` (custom wrapper with keyboard fix) and `components/Button.tsx` show the team already maintains thin wrappers and owns their component code. CLAUDE.md emphasizes explicit, auditable code over clever tricks. Allows feature teams to work in parallel (one team handles Text migration, another ships new features simultaneously). Clear success metric: all import sites updated, tests pass, device testing validates behavior. + +**Downsides:** Takes weeks, not days (~1 week per 3-4 components given 87 files). Requires discipline to avoid ad-hoc deviations mid-migration (e.g., "while we're migrating Button, let's also refactor its styles"). Mechanical find-and-replace errors are possible (missing an import site leaves dead code). No single "migration complete" moment; rather, a gradual completion per component. + +**Confidence:** 95% + +**Complexity:** Low + +**Status:** Unexplored + +--- + +### 2. Two-Layer Stable API (Linux HAL Strategy) +**Description:** Define a minimal, version-stable public API in `packages/ui/` that acts as an adapter layer. This layer wraps only the core components PackRat actually uses (Text, Button, LargeTitleHeader, AlertMethods, Sheet, ListItem). Decouple the implementation: `@packrat-ai/nativewindui` remains the underlying implementation, but PackRat depends on a type and export contract that guarantees stability. When nativewindui updates and type names break (AlertRef → AlertMethods), the breaking change is absorbed *inside* the adapter layer, never exposed to consumers. Implementation: remap types via adapter (`export { Alert as Alert, type AlertMethods as AlertMethods }` + type-only adapters). + +**Rationale:** Borrowed from Linux kernel's Hardware Abstraction Layer (HAL) pattern. Kernel internals change; driver code targets the stable HAL, not kernel internals. Institutional grounding: team discovered that platform bugs are easier to fix and diagnose when wrapper layers are thin and their boundaries are explicit. This adapter layer adds ~40-50 lines of code but provides a strong stability contract. Works in parallel with Idea #1 (Monomorphism): migrate Text → adapter guarantees the stable interface → downstream callers update imports once per component, not many times per nativewindui version bump. Reduces the blast radius of upstream breaking changes. + +**Downsides:** Adds a layer, which is "one more indirection to understand." Requires discipline to keep the adapter surface minimal and avoid re-exporting unnecessary types or utilities. Requires updating the adapter each time nativewindui updates, but this is a single-file change (packages/ui/nativewindui/index.ts). Type remapping can be fragile if nativewindui's internal types are complex. + +**Confidence:** 90% + +**Complexity:** Low + +**Status:** Unexplored + +--- + +### 3. Dual-Mode Wrapper (Fast + Slow Path) +**Description:** Keep nativewindui as the legacy "slow path" in `packages/ui/nativewindui` (unchanged). Introduce a new "fast path" with thin direct re-exports in `apps/expo/components/{Text,Button,LargeTitleHeader}.tsx` that wrap @expo/ui or rn-primitives directly. Both paths export the same interface. Screens and features can opt-in to the fast path without coordinating a massive migration. Feature teams migrate their imports as they touch a component during normal refactoring; no forced cutover date or migration sprint required. + +**Rationale:** Automotive sidecar pattern: applications ran in lightweight containers, but high-velocity features (animations, gestures) used native code directly when container overhead was noticeable. Works because the interface is identical, so callers don't care which implementation they're using. Grounded in reality: `apps/expo/components/TextInput.tsx` and `apps/expo/components/Button.tsx` already exist as thin custom wrappers; the team *likes* owning their component code, maintaining thin wrappers, and having tight feedback loops. Dual-mode + Monomorphism means feature teams drive the migration organically (when the packing-list team refactors, they migrate to fast path; when a feature stays stable, legacy path is fine). Massively reduces coordination burden and shipping delays. + +**Downsides:** Maintains two code paths in parallel (increases test matrix slightly—need to test both paths for each component). Requires clear signposting and team convention ("always use fast path for new code, legacy path is for compatibility only"). Teams can diverge if not disciplined (some routes use old, some use new). Slightly higher cognitive load during transition period. + +**Confidence:** 93% + +**Complexity:** Low-Medium + +**Status:** Unexplored + +--- + +### 4. Feature-Flag Component Versions (Staged Rollout) +**Description:** Add feature-flag entries to `apps/expo/config.ts` (e.g., `COMPONENT_TEXT_USE_EXPO_UI: false`, `COMPONENT_BUTTON_USE_EXPO_UI: false`). New components ship with the flag off by default. Conditional rendering in code: if flag is true, use @expo/ui component; else, use nativewindui fallback. Release 1 (preview): flag still off (legacy path active). Release 2 (preview): enable the flag in preview builds only, collect telemetry (Sentry errors, performance, UX metrics) from preview users. Release 3 (prod): if all signals green, ship to 10% of production users (if EAS Segments supports it), then roll to 100%. Revert to nativewindui implementation zero-cost if a regression emerges (just flip the flag off). + +**Rationale:** Feature flags are the team's established pattern (already in `apps/expo/config.ts`, EAS profiles are configured). Turns each component migration into a low-risk experiment with instrumented rollback. Grounded in capability: team has EAS Updates, so new component logic can ship without triggering an app store rebuild. Decouples component adoption from SDK releases. Enables continuous design iteration without the traditional "we must wait for iOS App Store review and Android Play Store review" cadence. Supports A/B testing if needed (show @expo/ui Button to 50% of users, nativewindui to the other 50%, compare adoption/satisfaction). + +**Downsides:** Adds conditional logic to every component (slight code complexity, requires careful testing of both branches). Requires thinking about what "flag off" means (usually: fall back to nativewindui implementation or keep current behavior). Requires Sentry + analytics to be configured so regressions surface (crashing, poor performance). If a component has many props, conditional rendering can become verbose. Risk of bit rot (a flag left off indefinitely, code path becomes stale). + +**Confidence:** 92% + +**Complexity:** Low-Medium + +**Status:** Unexplored + +--- + +### 5. Vertical Code Reuse (Mobile-Web Convergence) +**Description:** Recognize that rn-primitives (used on mobile via nativewindui) and Radix UI (used on web via Shadcn in `apps/guides` and `apps/landing`) share a compositional foundation. After migrating mobile to rn-primitives-based components, build a bridge package (`@packrat/rn-primitives-web` or similar) that wraps Shadcn components with rn-primitives interfaces. This allows form components, buttons, checkboxes, inputs, etc. to run on all three platforms (iOS/Android/web) from the *same* TypeScript source. Example: a "multiselect items" component built on `@rn-primitives/checkbox` becomes reusable on mobile and web without duplication. + +**Rationale:** Strategic multiplier. Today, TextInput on mobile (RN TextInput) and web (Shadcn input) are completely separate codebases. Same for Button, Checkbox, Switch, Select. Unified primitives-based API means form logic, validation, error states, and event handling code compile to both platforms from one source file. Each new form feature (AI-assisted packing suggestions, trip creation flow, outfit builder) lands everywhere automatically. Grounded in market signal: rn-primitives (v1.4.0, March 2026) is actively maintained, has 10+ primitives (alert-dialog, avatar, checkbox, context-menu, dropdown-menu, etc.) that map cleanly to Radix UI's component set. High leverage: form-heavy features become 3x faster to ship. + +**Downsides:** Requires design discipline (form components must stay on the rn-primitives interface boundary, not diverge to platform-specific styling tricks). Initial design work is significant (~2-4 weeks to design @packrat/rn-primitives-web adapter + refactor one form as proof-of-concept). Long-term payoff is high but back-loaded (value compounds over quarters as more forms use the bridge). Requires web and mobile teams to coordinate on form contracts. + +**Confidence:** 85% + +**Complexity:** Medium-High + +**Status:** Unexplored + +--- + +### 6. Visual Snapshots + Auto-Regression Detection +**Description:** Snapshot-test the app's visual rendering of each component under both old (nativewindui) and new (@expo/ui) implementations. Use Percy or Detox visual testing tools. For each component migration: old-implementation → screenshot → new-implementation → screenshot → pixel-diff. If they match (pixel-perfect or perceptual distance < 5%), the migration is safe. Before shipping a migrated component to production, require: (1) unit tests pass, (2) E2E tests pass, (3) visual snapshots match old behavior, (4) Maestro flows pass on both iOS and Android. + +**Rationale:** Game engines solved "will my game work on UE5 vs. Godot?" by comparing visual output objectively. PackRat learned that AlertRef → AlertMethods broke 18+ files, and type-only fixes might mask rendering regressions. Visual snapshots catch subtle layout shifts, color mismatches, and spacing changes that code review might miss. Grounded in capability: `apps/expo` already runs Maestro E2E tests. Visual snapshot infra (Percy, Detox visual checks) integrates with existing test harness (CI already configured for Maestro). Doesn't require human code review of all 87 files; just "render and diff." + +**Downsides:** Requires tooling setup (Percy or Detox integration, ~2-4 hours of CI configuration). Snapshots can be flaky if rendering is non-deterministic (animations, network data loading, dynamic text). Requires a baseline (initial snapshot) that might itself have bugs, so snapshots validate "change is consistent" not "change is correct." Snapshot reviews are manual (diff reviews can be tedious). + +**Confidence:** 88% + +**Complexity:** Medium + +**Status:** Unexplored + +--- + +### 7. Automate Import Migration with Codemod +**Description:** Write a jscodeshift codemod that rewrites all `@packrat/ui/nativewindui` imports to their replacement paths (@expo/ui, rn-primitives, or local components). The codemod patterns matches ~90 import sites automatically (Text: 27 uses → import from new path, Button: 15 uses → new path, etc.). For custom components (LargeTitleSearchBar, FormSection), the codemod leaves them untouched or stubs them with a deprecation comment. Run locally: `jscodeshift -t migration-codemod.js apps/expo/`. Validate output, commit changes. Mechanical work (177 imports across 87 files) becomes a single command + testing. + +**Rationale:** Automates the most tedious part of Monomorphism. Team already has precedent: `packages/api/scripts/lint/...` scripts validate generated code in CI. Codemods are standard in TypeScript ecosystem (major framework migrations, e.g., React, use jscodeshift). Reduces human errors (typos, missed imports, inconsistent refactoring). Makes bulk migration mechanical and repeatable—if the codemod is correct once, it's correct every time. Grounded in codebase: 177 imports are high but consistent (mostly Text, Button, Sheet, LargeTitleHeader, ListItem); pattern-matching is straightforward. + +**Downsides:** Requires writing + maintaining the codemod (1-2 days upfront). If component APIs differ significantly (e.g., nativewindui's custom props don't exist in @expo/ui), the codemod can't auto-fix everything; those require manual migration or an interim wrapper. Error recovery is manual (if the codemod produces bad code, you must fix it or revert and refine the codemod). Codemod must be validated on a test branch before running on main. + +**Confidence:** 87% + +**Complexity:** Low-Medium + +**Status:** Unexplored + +--- + +## Rejection Summary + +**27 ideas rejected.** Common rejection reasons: + +- **Duplicates / Subsumed:** Removing wrapper layer entirely (high value but extreme burden; #2 provides 80% benefit with 20% cost). Unblock type-checking (subsumed by #2 adapter layer). Code generation (subsumed by #2). Copy-own UI library (alternative path, but requires ongoing maintenance; primary path is lower effort). +- **Over-Engineering:** Styling primitives + behavioral wrappers (redundant with #2). Component library as sub-libraries (monorepo is simpler). Compiler transform / TypeScript plugin (too expensive for bulk migration; codemod #7 is lighter). +- **Not Aligned with Strategy:** Platform-specific divergence (contradicts PackRat's unified design). Remove CSS (solves non-existent problem; NativeWind is working). Runtime component swapping (powerful but more complex than feature flags). +- **Too Minor:** Eliminate GitHub token (onboarding friction, not blocking). Remove web duplications (1 file, nice-to-have). +- **Not Actionable:** Two-week blitz (stress-driven delivery, unrealistic timeline). Lock in convergence window (artificial deadline). Island components (valuable long-term, but premature before understanding migration impact). + +--- + +## Implementation Sequence (Recommended) + +1. **Start:** #2 (Two-Layer Stable API) — set up adapter layer in packages/ui/ as infrastructure for stability. +2. **Parallel:** #1 (Monomorphism) — start with Text component (27 imports), single PR, test thoroughly. +3. **Parallel:** #7 (Automate) — write codemod for next components (Button, LargeTitleHeader). +4. **Validate:** #6 (Visual Snapshots) — set up before shipping migrated components; each component verified. +5. **Rollout:** #4 (Feature Flags) — gate new components, preview testing before prod. +6. **Integrate:** #3 (Dual-Mode) — teams migrate incrementally as they touch features; no forced cutover. +7. **Extend:** #5 (Mobile-Web Reuse) — once core components (Button, Text, Input, Sheet) are stable, build rn-primitives-web adapter for form reuse. + +--- + +## Quality Bar + +- ✅ Grounded in codebase context (Expo SDK 56, RN 0.85, 177 imports across 87 files) +- ✅ Candidates generated before filtering (47 ideas across 6 ideation frames) +- ✅ Many-ideas → critique → survivors mechanism preserved +- ✅ Every rejected idea has a documented reason +- ✅ Survivors form coherent strategy (not just individual improvements) +- ✅ Pragmatism prioritized (boring beats clever; existing infrastructure reused) +- ✅ Leverage identified (cross-platform code reuse, ecosystem contribution) diff --git a/docs/migrations/nativewindui-to-expo-ui.md b/docs/migrations/nativewindui-to-expo-ui.md new file mode 100644 index 0000000000..ece8e85d28 --- /dev/null +++ b/docs/migrations/nativewindui-to-expo-ui.md @@ -0,0 +1,162 @@ +--- +started: 2026-06-14 +status: in-progress +tracking: packages/ui/nativewindui/index.ts +progress-cmd: bun check:migration +--- + +# NativeWindUI → Expo UI Migration + +## Why + +NativeWindUI was chosen for native look and feel. Expo UI now provides that directly — via SwiftUI on iOS and Jetpack Compose on Android — without requiring a private GitHub Packages token, without type-breaking changes on every upstream release, and without wrapper opacity hiding platform bugs. + +## Rules + +1. **`@expo/ui` is the primary source.** Every component gets its replacement from `@expo/ui` first. +2. **Universal before platform-specific.** `@expo/ui` Universal components run on iOS, Android, and web from one file. Use them when available. Use SwiftUI/JC platform-specific variants only when Universal doesn't cover the use case. +3. **RN core is last resort.** Only fall back to `react-native` when `@expo/ui` has no equivalent (e.g. `useColorScheme`). +4. **All UI lives in `packages/ui`.** No UI components in `apps/expo/components/` — those are either being replaced or moved. +5. **`apps/expo/components/` cleanup is a parallel track** — see section below. + +## How progress is tracked + +`packages/ui/nativewindui/index.ts` is the live tracker — one export line per component still backed by `@packrat-ai/nativewindui`. Delete a line when its `packages/ui` replacement lands. When the file is empty, remove `@packrat-ai/nativewindui` from `packages/ui/package.json` and drop `PACKRAT_NATIVEWIND_UI_GITHUB_TOKEN` from `bunfig.toml`. + +```bash +bun check:migration # prints per-phase count + fails if any file bypasses the adapter +``` + +## Replacement map + +Priority column: **U** = `@expo/ui` Universal, **S** = `@expo/ui` SwiftUI (iOS), **JC** = `@expo/ui` Jetpack Compose (Android), **C** = `@expo/ui` community drop-in, **RN** = `react-native` (last resort only), **ER** = `expo-router`. + +| NativeWindUI | Uses | @expo/ui replacement | Priority | packages/ui file | +|---|---|---|---|---| +| `Text` | 114 | `Text` | U | `src/text.tsx` | +| `Button` | 49 | `Button` | U | `src/button.tsx` | +| `ActivityIndicator` | 22 | `ProgressView` / `LoadingIndicator` | S + JC | `src/loading-indicator.ios.tsx` + `.android.tsx` | +| `ListItem` | 21 | `ListItem` (+ `.Leading` `.Trailing` `.Supporting`) | U | `src/list-item.tsx` | +| `LargeTitleHeader` + `LargeTitleSearchBarMethods` | 25 | `Stack.Screen.Title` + `Stack.SearchBar` + `Stack.Toolbar` | ER | — (navigation layer, not packages/ui) | +| `Alert` + `AlertMethods` + `AlertAnchor` | 25 | `Alert` / `AlertDialog` + `BasicAlertDialog` | S + JC | `src/alert.ios.tsx` + `.android.tsx` | +| `Sheet` + `useSheetRef` | 16 | `BottomSheet` | U | `src/bottom-sheet.tsx` | +| `Form` | 8 | `Form` / `FieldGroup` | S + U | `src/form.ios.tsx` + `.tsx` | +| `FormSection` | 8 | `Section` / `FieldGroup.Section` | S + U | `src/form-section.ios.tsx` + `.tsx` | +| `FormItem` | 8 | `LabeledContent` / `FieldGroup.Section` row | S + U | part of form-section | +| `TextField` | 9 | `TextInput` | U | `src/text-input.tsx` | +| `Card` + `CardContent` + `CardTitle` | 8 | `Card` / custom `View` | JC + custom iOS | `src/card.android.tsx` + `.ios.tsx` | +| `SegmentedControl` | 3 | `SegmentedControl` | C | `src/segmented-control.tsx` | +| `Toggle` | 1 | `Switch` | U | `src/switch.tsx` | +| `List` | 1 | `List` | U | `src/list.tsx` | +| `ContextMenuMethods` | 1 | `ContextMenu` / `DropdownMenu` | S + JC | `src/context-menu.ios.tsx` + `.android.tsx` | +| `SearchInput` / `AdaptiveSearchHeader` | 1 | `Stack.SearchBar` | ER | — (navigation layer) | +| `Avatar` + `AvatarFallback` + `AvatarImage` | 6 | `@rn-primitives/avatar` (no @expo/ui Avatar) | — | `src/avatar.tsx` | +| `useColorScheme` | 20 | `useColorScheme` from `react-native` (no @expo/ui hook) | RN | — (hook, not a component) | +| `cn` | 3 | remove — import `tailwind-merge` directly | — | — (utility, not a component) | + +## `apps/expo/components/` cleanup (parallel track) + +Everything here is either replaced by `@expo/ui` via `packages/ui` or moved to a feature folder. Nothing new should be added here. + +| File | Action | +|---|---| +| `Button.tsx` | Delete — replaced by `packages/ui` `Button` | +| `Card.tsx` | Delete — replaced by `packages/ui` `Card` | +| `TextInput.tsx` | Delete — replaced by `packages/ui` `TextInput` (port the Android keyboard fix into it) | +| `SearchInput.tsx` | Delete — replaced by `Stack.SearchBar` / `headerSearchBarOptions` | +| `ThemeToggle.tsx` | Move to `packages/ui/src/theme-toggle.tsx` | +| `Container.tsx` | Move to `packages/ui/src/container.tsx` | +| `ErrorState.tsx` | Move to `packages/ui/src/error-state.tsx` | +| `ScreenContent.tsx` | Move to `packages/ui/src/screen-content.tsx` | +| `Markdown.tsx` | Move to `packages/ui/src/markdown.tsx` | +| `Icon/` | Move to `packages/ui/src/icon/` — wraps `@expo/ui` Universal `Icon` | +| `LargeTitleHeaderOverlapFixIOS.tsx` | Delete — workaround no longer needed after Phase 2 | +| `LargeTitleHeaderSearchContentContainer.tsx` | Delete — same | +| `AndroidTabBarInsetFix.tsx` | Move to `packages/ui/src/android-tab-bar-inset-fix.android.tsx` | +| `BackButton.tsx` | Move to `packages/ui/src/back-button.tsx` | +| `HeaderButton.tsx` | Move to `packages/ui/src/header-button.tsx` | +| `TabBarIcon.tsx` | Move to `packages/ui/src/tab-bar-icon.tsx` | +| `CategoriesFilter.tsx` | Move to `apps/expo/features/catalog/components/` | +| `ai-chatHeader.tsx` | Move to `apps/expo/features/ai-chat/components/` | +| `EditScreenInfo.tsx` | Delete (dev-only artefact) | +| `initial/` | Audit each file — move to relevant feature folder or `packages/ui` | + +## Phases + +### Phase 1 — Non-UI cleanup (no device testing needed) +Remove utilities from the adapter that were never UI components. + +- `useColorScheme` → change 20 import sites to `react-native` +- `cn` → inline the 3 call sites with `tailwind-merge` or delete + +Estimated effort: half a day. Ship as one PR. + +### Phase 2 — Expo Router native patterns (42 uses) +Replace ref-based imperative navigation APIs. + +- `LargeTitleHeader` → restructure each screen to use `Stack.Screen.Title`, `Stack.SearchBar`, `Stack.Toolbar`. Each screen group (home, packs, catalog, trips, profile) is one sub-PR. +- `Sheet` + `useSheetRef` → replace `ref.current.present()` calls with `router.push('/sheet-route')` + `presentation: 'formSheet'` in the Stack layout. New route files replace old modal components. +- `SearchInput` / `AdaptiveSearchHeader` → `Stack.SearchBar` or `headerSearchBarOptions`. +- Delete `LargeTitleHeaderOverlapFixIOS.tsx` and `LargeTitleHeaderSearchContentContainer.tsx` once their screens are migrated. + +Estimated effort: 3–5 days. One PR per tab section. + +### Phase 3 — Universal @expo/ui components in packages/ui (high frequency) +Wire up `packages/ui/src/` files that re-export or wrap Universal components. Import sites change from `@packrat/ui/nativewindui` to `@packrat/ui`. + +Order by frequency: + +1. **`Text` (114 uses)** — `packages/ui/src/text.tsx` wrapping `@expo/ui` Universal `Text`. Preserve the `className` prop via a thin NativeWind shim so call sites only change the import path, not the JSX. +2. **`Button` (49 uses)** — `packages/ui/src/button.tsx` wrapping `@expo/ui` Universal `Button`. Map nativewindui variant/size props to `@expo/ui` equivalents. +3. **`ListItem` (21 uses)** — `packages/ui/src/list-item.tsx` re-exporting `@expo/ui` Universal `ListItem` with `Leading`, `Trailing`, `Supporting` sub-components. +4. **`Sheet` + `useSheetRef` (16 uses)** — `packages/ui/src/bottom-sheet.tsx` wrapping `@expo/ui` Universal `BottomSheet`. Replace `useSheetRef` with `isPresented` / `onDismiss` props. +5. **`Form` + `FormSection` + `FormItem` (24 uses)** — `packages/ui/src/form.tsx` wrapping `FieldGroup` + `FieldGroup.Section` (Universal); `packages/ui/src/form.ios.tsx` wrapping SwiftUI `Form` + `Section` for native iOS grouped lists. +6. **`TextField` (9 uses)** — `packages/ui/src/text-input.tsx` wrapping `@expo/ui` Universal `TextInput`. Port the Android keyboard focus fix from the existing `apps/expo/components/TextInput.tsx`. +7. **`Toggle` (1 use)** — `packages/ui/src/switch.tsx` re-exporting `@expo/ui` Universal `Switch`. +8. **`List` (1 use)** — `packages/ui/src/list.tsx` re-exporting `@expo/ui` Universal `List`. + +Estimated effort: 1 week across PRs. + +### Phase 4 — Platform-specific @expo/ui wrappers in packages/ui +Components that need per-platform files because @expo/ui has different APIs on iOS vs Android. + +- **`ActivityIndicator` (22 uses)** — `packages/ui/src/loading-indicator.ios.tsx` (SwiftUI `ProgressView`) + `packages/ui/src/loading-indicator.android.tsx` (JC `LoadingIndicator`). Metro picks up the platform file automatically. +- **`Alert` + `AlertMethods` + `AlertAnchor` (25 uses)** — `packages/ui/src/alert.ios.tsx` (SwiftUI `Alert` with `Alert.Trigger`, `Alert.Actions`, `Alert.Message`) + `packages/ui/src/alert.android.tsx` (JC `AlertDialog` / `BasicAlertDialog`). +- **`Card` family (8 uses)** — `packages/ui/src/card.android.tsx` (JC `Card`) + `packages/ui/src/card.ios.tsx` (custom `View`-based card, SwiftUI has no `Card` primitive). +- **`ContextMenuMethods` (1 use)** — `packages/ui/src/context-menu.ios.tsx` (SwiftUI `ContextMenu` with `Trigger`, `Items`, `Preview`) + `packages/ui/src/context-menu.android.tsx` (JC `DropdownMenu`). +- **`SegmentedControl` (3 uses)** — `packages/ui/src/segmented-control.tsx` re-exporting from `@expo/ui/community/segmented-control`. + +Estimated effort: 3–5 days. + +### Phase 5 — No @expo/ui equivalent +- **`Avatar` family (6 uses)** — `packages/ui/src/avatar.tsx` wrapping `@rn-primitives/avatar` (same foundation nativewindui used internally; only import path changes). + +Estimated effort: half a day. + +### Phase 6 — packages/ui restructure + apps/expo/components/ removal +- Add `packages/ui/src/` directory with proper exports and `tsconfig.json` path alias. +- Move / delete all files from `apps/expo/components/` per the cleanup table above. +- Update root `tsconfig.json` and `packages/ui/package.json` to export from `src/` instead of wrapping `@packrat-ai/nativewindui`. +- Enable `check-types` in `packages/ui/package.json` (currently disabled because tsc deep-checked nativewindui source `.tsx` files and surfaced 197 upstream errors — that problem goes away once we own the source). + +Estimated effort: 1–2 days. + +### Phase 7 — Final removal +Once the adapter file is empty and all `apps/expo/components/` files are gone: + +- Remove `@packrat-ai/nativewindui` from `packages/ui/package.json`. +- Remove the `@packrat-ai` scope from `bunfig.toml`. +- Remove `PACKRAT_NATIVEWIND_UI_GITHUB_TOKEN` from `.env.local` docs, CI secrets, and the `CLAUDE.md` private package auth section. +- Delete `packages/ui/nativewindui/` directory. +- Delete `scripts/lint/nativewindui-migration.ts` and remove `check:migration` from `package.json`. + +## PR checklist per component + +- [ ] Replacement lives in `packages/ui/src/` +- [ ] Platform-specific files (`.ios.tsx` / `.android.tsx`) used where needed +- [ ] Renders correctly on iOS + Android (dark mode included) +- [ ] Existing Maestro E2E flows pass +- [ ] `bun check:migration` exits 0 +- [ ] `bun check-types` exits 0 +- [ ] Corresponding line(s) removed from `packages/ui/nativewindui/index.ts` +- [ ] No remaining imports from `apps/expo/components/` for migrated component diff --git a/package.json b/package.json index 9286f99e4e..0e8b542534 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "check:deps": "manypkg check", "check:magic-strings": "bun run --cwd packages/checks check:magic-strings", "check:package-json": "bun scripts/format/sort-package-json.ts --check", + "check:migration": "bun scripts/lint/nativewindui-migration.ts", "check:react-doctor": "bun scripts/lint/check-react-doctor.ts", "check-types": "bun scripts/check-types.ts", "check-types:packages": "turbo run check-types --filter='./apps/*' --filter='./packages/*'", diff --git a/packages/ui/nativewindui/index.ts b/packages/ui/nativewindui/index.ts index 19556f3fcc..fb134d2ffb 100644 --- a/packages/ui/nativewindui/index.ts +++ b/packages/ui/nativewindui/index.ts @@ -1 +1,56 @@ -export * from '@packrat-ai/nativewindui'; +// NativeWindUI → Expo UI migration tracker +// +// Each export line is one component still backed by @packrat-ai/nativewindui. +// Delete a line when its packages/ui/src/ replacement lands. +// When this file is empty: remove @packrat-ai/nativewindui from package.json +// and drop PACKRAT_NATIVEWIND_UI_GITHUB_TOKEN from bunfig.toml. +// +// Run `bun check:migration` for per-phase progress. +// Full plan: docs/migrations/nativewindui-to-expo-ui.md +// +// Phase 1 ✓ done — useColorScheme → expo-app/lib/hooks/useColorScheme, cn → expo-app/lib/cn +// Phase 2 ✓ done — LargeTitleHeader/SearchInput → Stack.Screen + headerSearchBarOptions +// +// Phase 3 — @expo/ui Universal → packages/ui/src/ +export { Text, TextClassContext, textVariants } from '@packrat-ai/nativewindui'; // 114 uses → @expo/ui Universal Text +export { Button, buttonVariants, buttonTextVariants } from '@packrat-ai/nativewindui'; // 49 uses → @expo/ui Universal Button +export type { ButtonProps } from '@packrat-ai/nativewindui'; +export { + ListItem, + List, + ListSectionHeader, + getStickyHeaderIndices, +} from '@packrat-ai/nativewindui'; // 22 uses → @expo/ui Universal ListItem + List +export type { + ListDataItem, + ListItemProps, + ListProps, + ListRef, + ListRenderItemInfo, + ListSectionHeaderProps, +} from '@packrat-ai/nativewindui'; +export { Sheet, useSheetRef } from '@packrat-ai/nativewindui'; // 16 uses → @expo/ui Universal BottomSheet +export { Form, FormSection, FormItem } from '@packrat-ai/nativewindui'; // 24 uses → @expo/ui Universal FieldGroup + SwiftUI Form +export { TextField } from '@packrat-ai/nativewindui'; // 9 uses → @expo/ui Universal TextInput +export type { TextFieldProps, TextFieldRef } from '@packrat-ai/nativewindui'; +export { Toggle } from '@packrat-ai/nativewindui'; // 1 use → @expo/ui Universal Switch +// +// Phase 4 — @expo/ui platform-specific wrappers (.ios.tsx + .android.tsx) in packages/ui/src/ +export { ActivityIndicator } from '@packrat-ai/nativewindui'; // 22 uses → ProgressView (iOS) + LoadingIndicator (Android) +export { Alert, AlertAnchor } from '@packrat-ai/nativewindui'; // 14 uses → @expo/ui SwiftUI Alert + JC AlertDialog +export type { AlertMethods } from '@packrat-ai/nativewindui'; // 14 uses +export { + Card, + CardContent, + CardTitle, + CardBadge, + CardDescription, + CardFooter, + CardImage, + CardSubtitle, +} from '@packrat-ai/nativewindui'; // 8 uses → JC Card (Android) + custom View (iOS) +export { SegmentedControl } from '@packrat-ai/nativewindui'; // 3 uses → @expo/ui community SegmentedControl +export type { ContextMenuMethods } from '@packrat-ai/nativewindui'; // 1 use → SwiftUI ContextMenu + JC DropdownMenu +// +// Phase 5 — no @expo/ui equivalent +export { Avatar, AvatarFallback, AvatarImage } from '@packrat-ai/nativewindui'; // 6 uses → @rn-primitives/avatar diff --git a/scripts/lint/nativewindui-migration.ts b/scripts/lint/nativewindui-migration.ts new file mode 100644 index 0000000000..4b995c0862 --- /dev/null +++ b/scripts/lint/nativewindui-migration.ts @@ -0,0 +1,99 @@ +#!/usr/bin/env bun +// +// nativewindui-migration.ts — tracks NativeWindUI → Expo UI migration progress. +// +// Reads packages/ui/nativewindui/index.ts and counts remaining export lines per +// phase. Scans apps/expo for any direct imports from @packrat-ai/nativewindui +// (which bypass the adapter and indicate a stale callsite). +// +// Exit codes: +// 0 — clean (no violations; progress printed to stdout) +// 1 — violations found (direct imports bypassing adapter) + +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +const ROOT = join(import.meta.dir, '..', '..'); +const ADAPTER = join(ROOT, 'packages/ui/nativewindui/index.ts'); +const EXPO_SRC = join(ROOT, 'apps/expo'); + +// ── 1. Count remaining exports per phase ──────────────────────────────────── + +const adapterLines = readFileSync(ADAPTER, 'utf8').split('\n'); + +type Phase = '1' | '2' | '3' | '4' | '5'; +const phaseCounts: Record = { '1': 0, '2': 0, '3': 0, '4': 0, '5': 0 }; +const phaseNames: Record = { + '1': 'utilities (non-UI) ', + '2': 'expo-router native patterns ', + '3': '@expo/ui Universal → packages/ui ', + '4': '@expo/ui platform-specific ', + '5': 'no @expo/ui equivalent ', +}; + +// Snapshot total at migration start (update if new components are added to the adapter). +// Count: p1=2, p2=5, p3=10, p4=6, p5=1 = 24 tracked export groups +const TOTAL_AT_START = 24; + +let currentPhase: Phase = '1'; +for (const line of adapterLines) { + const phaseMatch = line.match(/Phase (\d)/); + if (phaseMatch) { + currentPhase = phaseMatch[1] as Phase; + continue; + } + if (line.startsWith('export')) { + phaseCounts[currentPhase]++; + } +} + +const totalRemaining = Object.values(phaseCounts).reduce((a, b) => a + b, 0); +const totalMigrated = TOTAL_AT_START - totalRemaining; +const pct = Math.round((totalMigrated / TOTAL_AT_START) * 100); + +console.log('\n── NativeWindUI → Expo UI migration progress ───────────────────'); +for (const [phase, name] of Object.entries(phaseNames) as [Phase, string][]) { + const count = phaseCounts[phase]; + const status = count === 0 ? '✓ done' : `${count} remaining`; + console.log(` Phase ${phase} ${name} ${status}`); +} +console.log(`\n Overall: ${totalMigrated}/${TOTAL_AT_START} export groups migrated (${pct}%)`); +console.log('─────────────────────────────────────────────────────────────────\n'); + +// ── 2. Scan for direct @packrat-ai/nativewindui imports (adapter bypass) ─── + +const EXCLUDED = new Set(['node_modules', 'dist', 'build', '.expo', '.wrangler']); + +function walk(dir: string): string[] { + const results: string[] = []; + for (const entry of readdirSync(dir)) { + if (EXCLUDED.has(entry)) continue; + const full = join(dir, entry); + if (statSync(full).isDirectory()) { + results.push(...walk(full)); + } else if (/\.(ts|tsx)$/.test(entry)) { + results.push(full); + } + } + return results; +} + +const violations: string[] = []; +for (const file of walk(EXPO_SRC)) { + const content = readFileSync(file, 'utf8'); + if (content.includes("from '@packrat-ai/nativewindui'")) { + const rel = file.replace(ROOT + '/', ''); + const line = + content.split('\n').findIndex((l) => l.includes("from '@packrat-ai/nativewindui'")) + 1; + violations.push(` ${rel}:${line}`); + } +} + +if (violations.length > 0) { + console.error('✗ Direct @packrat-ai/nativewindui imports found (use @packrat/ui/nativewindui):'); + for (const v of violations) console.error(v); + console.error(''); + process.exit(1); +} + +console.log('✓ No adapter bypass violations found.\n'); From 53e8790a241aba12e2e05fbb5f3bdffdd8f5d645 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Sun, 14 Jun 2026 14:21:37 +0100 Subject: [PATCH 03/17] chore: sort root package.json fields --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e8b542534..1c8d5056b3 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "check:coverage:update": "bun run scripts/lint/coverage-baseline-update.ts", "check:deps": "manypkg check", "check:magic-strings": "bun run --cwd packages/checks check:magic-strings", - "check:package-json": "bun scripts/format/sort-package-json.ts --check", "check:migration": "bun scripts/lint/nativewindui-migration.ts", + "check:package-json": "bun scripts/format/sort-package-json.ts --check", "check:react-doctor": "bun scripts/lint/check-react-doctor.ts", "check-types": "bun scripts/check-types.ts", "check-types:packages": "turbo run check-types --filter='./apps/*' --filter='./packages/*'", From 82bbc90172eca62406d84f2679f7c0949b6b8c43 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 15 Jun 2026 09:33:26 +0100 Subject: [PATCH 04/17] feat(migration): add SearchOverlay for animated search experience on all list screens Platform-specific component ported from LargeTitleHeader reference: - iOS (SearchOverlay.ios.tsx): headerSearchBarOptions + FadeIn absoluteFill content overlay when focused or has text - Android (SearchOverlay.tsx): magnify icon in headerRight opens a full-screen Portal overlay with pill scale animation (custom Reanimated worklet), FadeInRight TextInput, FadeInUp content area, back-arrow dismiss, hardware-back-button support Applied to all 6 screens that had LargeTitleHeader + searchBar: home, catalog, packs, guides, pack-templates, trips Screens with a create button (packs, pack-templates, trips) pass androidHeaderRightActions so the create button and search icon are composed together in headerRight on Android; on iOS the parent Stack.Screen headerRight is unchanged and merges with SearchOverlay's Stack.Screen (headerSearchBarOptions only). Full parity with pre-migration behavior: - Same placeholder strings per screen - Same search results / empty-state / loading content - "Type to search" prompt when overlay open but query is empty --- apps/expo/app/(app)/(tabs)/(home)/index.tsx | 49 +++--- .../SearchOverlay/SearchOverlay.ios.tsx | 35 +++++ .../SearchOverlay/SearchOverlay.tsx | 147 ++++++++++++++++++ apps/expo/components/SearchOverlay/index.ts | 2 + apps/expo/components/SearchOverlay/types.ts | 13 ++ .../catalog/screens/CatalogItemsScreen.tsx | 24 +-- .../guides/screens/GuidesListScreen.tsx | 14 +- .../screens/PackTemplateListScreen.tsx | 21 +-- .../features/packs/screens/PackListScreen.tsx | 30 ++-- .../features/trips/screens/TripListScreen.tsx | 15 +- apps/expo/lib/i18n/locales/en.json | 3 +- 11 files changed, 291 insertions(+), 62 deletions(-) create mode 100644 apps/expo/components/SearchOverlay/SearchOverlay.ios.tsx create mode 100644 apps/expo/components/SearchOverlay/SearchOverlay.tsx create mode 100644 apps/expo/components/SearchOverlay/index.ts create mode 100644 apps/expo/components/SearchOverlay/types.ts diff --git a/apps/expo/app/(app)/(tabs)/(home)/index.tsx b/apps/expo/app/(app)/(tabs)/(home)/index.tsx index 4376fd307d..96d482f7a1 100644 --- a/apps/expo/app/(app)/(tabs)/(home)/index.tsx +++ b/apps/expo/app/(app)/(tabs)/(home)/index.tsx @@ -6,6 +6,7 @@ import type { ListDataItem } from '@packrat/ui/nativewindui'; import { List, type ListRenderItemInfo, ListSectionHeader } from '@packrat/ui/nativewindui'; import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; import { Icon } from 'expo-app/components/Icon'; +import { SearchOverlay } from 'expo-app/components/SearchOverlay'; import { appConfig, featureFlags } from 'expo-app/config'; import { AIChatTile } from 'expo-app/features/ai/components/AIChatTile'; import { ReportedContentTile } from 'expo-app/features/ai/components/ReportedContentTile'; @@ -246,13 +247,13 @@ export default function DashboardScreen() { title: t('dashboard.title'), headerLargeTitle: true, headerBackVisible: false, - headerSearchBarOptions: { - placeholder: appConfig.dashboard.strings.searchPlaceholder, - onChangeText: (e) => setSearchValue(e.nativeEvent.text), - }, }} /> - {searchValue ? ( + setSearchValue('')} > @@ -283,20 +284,30 @@ export default function DashboardScreen() { ) : null } - ListEmptyComponent={() => ( - - - - - {t('dashboard.noResults')} - - - {t('dashboard.tryDifferent')} - - - )} + ListEmptyComponent={() => + searchValue.trim() ? ( + + + + + {t('dashboard.noResults')} + + + {t('dashboard.tryDifferent')} + + + ) : ( + + + + + {t('dashboard.searchPrompt')} + + + ) + } /> - ) : null} + + onChangeText(e.nativeEvent.text), + onFocus: () => setIsFocused(true), + onBlur: () => setIsFocused(false), + }, + }} + /> + {(isFocused || value.length > 0) && ( + + {children} + + )} + + ); +} diff --git a/apps/expo/components/SearchOverlay/SearchOverlay.tsx b/apps/expo/components/SearchOverlay/SearchOverlay.tsx new file mode 100644 index 0000000000..4380914271 --- /dev/null +++ b/apps/expo/components/SearchOverlay/SearchOverlay.tsx @@ -0,0 +1,147 @@ +import { Portal } from '@rn-primitives/portal'; +import { Icon } from 'expo-app/components/Icon'; +import { SearchInput } from 'expo-app/components/SearchInput'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; +import { Stack } from 'expo-router'; +import { useCallback, useEffect, useId, useState } from 'react'; +import { BackHandler, Pressable, StyleSheet, View } from 'react-native'; +import Animated, { + FadeIn, + FadeInRight, + FadeInUp, + FadeOut, + FadeOutRight, + withTiming, +} from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import type { SearchOverlayProps } from './types'; + +export function SearchOverlay({ + placeholder, + value, + onChangeText, + children, + androidHeaderRightActions, +}: SearchOverlayProps) { + const [isOpen, setIsOpen] = useState(false); + const { colors } = useColorScheme(); + const insets = useSafeAreaInsets(); + const id = useId(); + + const close = useCallback(() => { + setIsOpen(false); + onChangeText(''); + }, [onChangeText]); + + useEffect(() => { + const handler = BackHandler.addEventListener('hardwareBackPress', () => { + if (isOpen) { + close(); + return true; + } + return false; + }); + return () => handler.remove(); + }, [isOpen, close]); + + return ( + <> + ( + + {androidHeaderRightActions} + setIsOpen(true)} hitSlop={8}> + + + + ), + }} + /> + {isOpen && ( + + + + + + + + + + + + { + if (value.length === 0) close(); + }} + autoCapitalize="none" + returnKeyType="search" + style={[styles.input, { color: colors.foreground }]} + placeholderTextColor={colors.grey2} + /> + + {!!value && ( + + onChangeText('')} hitSlop={8} className="p-2"> + + + + )} + + + + {children} + + + + )} + + ); +} + +const styles = StyleSheet.create({ + header: {}, + headerRightRow: { flexDirection: 'row', alignItems: 'center', gap: 4 }, + inputRow: { + height: 56, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingBottom: 10, + }, + inputFlex: { flex: 1 }, + input: { flex: 1, fontSize: 17, paddingHorizontal: 8 }, + content: { flex: 1 }, +}); + +const pillEntering = () => { + 'worklet'; + return { + initialValues: { transform: [{ scale: 1 }] }, + animations: { transform: [{ scale: withTiming(3, { duration: 400 }) }] }, + }; +}; + +const pillExiting = () => { + 'worklet'; + return { + initialValues: { transform: [{ scale: 3 }], opacity: 1 }, + animations: { transform: [{ scale: withTiming(1) }], opacity: withTiming(0) }, + }; +}; diff --git a/apps/expo/components/SearchOverlay/index.ts b/apps/expo/components/SearchOverlay/index.ts new file mode 100644 index 0000000000..427eb47145 --- /dev/null +++ b/apps/expo/components/SearchOverlay/index.ts @@ -0,0 +1,2 @@ +export { SearchOverlay } from './SearchOverlay'; +export type { SearchOverlayProps } from './types'; diff --git a/apps/expo/components/SearchOverlay/types.ts b/apps/expo/components/SearchOverlay/types.ts new file mode 100644 index 0000000000..936f68215f --- /dev/null +++ b/apps/expo/components/SearchOverlay/types.ts @@ -0,0 +1,13 @@ +import type { ReactNode } from 'react'; + +export interface SearchOverlayProps { + placeholder?: string; + value: string; + onChangeText: (text: string) => void; + children: ReactNode; + /** + * Android only — extra header-right content rendered before the search icon. + * On iOS the parent Stack.Screen headerRight handles this; this prop is ignored. + */ + androidHeaderRightActions?: ReactNode; +} diff --git a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx index 3edfd4d948..2b3184b9d6 100644 --- a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx +++ b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx @@ -4,6 +4,7 @@ import { searchValueAtom } from 'expo-app/atoms/itemListAtoms'; import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; import { CategoriesFilter } from 'expo-app/components/CategoriesFilter'; import { Icon } from 'expo-app/components/Icon'; +import { SearchOverlay } from 'expo-app/components/SearchOverlay'; import { withAuthWall } from 'expo-app/features/auth/hocs'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; @@ -123,15 +124,14 @@ function CatalogItemsScreen() { title: t('catalog.title'), headerLargeTitle: true, headerBackVisible: false, - headerSearchBarOptions: { - hideWhenScrolling: false, - onChangeText: (e) => setSearchValue(e.nativeEvent.text), - placeholder: t('catalog.searchPlaceholder'), - }, }} /> - {isSearching ? ( - isVectorLoading || !isQueryReady ? ( + + {isVectorLoading || !isQueryReady ? ( @@ -165,7 +165,7 @@ function CatalogItemsScreen() { {t('catalog.unableToSearch')} - ) : ( + ) : isSearching ? ( <> @@ -177,12 +177,16 @@ function CatalogItemsScreen() { {t('catalog.tryAdjustingFilters')} + ) : ( + + {t('catalog.searchPlaceholder')} + )} )} - ) - ) : null} + )} + { options={{ title: t('guides.guides'), headerLargeTitle: true, - headerSearchBarOptions: { - hideWhenScrolling: false, - onChangeText: (e) => handleSearch(e.nativeEvent.text), - placeholder: t('guides.searchPlaceholder'), - }, }} /> - {renderSearchContent()} + + {renderSearchContent()} + ( - - templateOptionsRef.current?.present()} /> - + templateOptionsRef.current?.present()} /> ), - headerSearchBarOptions: { - hideWhenScrolling: false, - onChangeText: (e) => setSearchValue(e.nativeEvent.text), - placeholder: t('packTemplates.searchPlaceholder'), - }, }} /> - {renderSearchContent()} + templateOptionsRef.current?.present()} /> + } + > + {renderSearchContent()} + , - headerSearchBarOptions: { - onChangeText: (e) => setSearchValue(e.nativeEvent.text), - }, }} /> - {searchValue ? ( - - ) : null} + } + > + {searchValue ? ( + + ) : ( + + {t('packs.searchPacks')} + + )} + , - headerSearchBarOptions: { - hideWhenScrolling: false, - onChangeText: (e) => setSearchValue(e.nativeEvent.text), - placeholder: t('trips.searchPlaceholder'), - }, }} /> - {renderSearchContent()} + } + > + {renderSearchContent()} + Date: Mon, 15 Jun 2026 09:55:33 +0100 Subject: [PATCH 05/17] fix(migration): move SearchOverlay to packages/ui, restore deleted components, fix transparent overlay - Move SearchOverlay from apps/expo/components/ to packages/ui/src/search-overlay/ - Fix transparent iOS overlay by adding bg-background to the absoluteFill Animated.View - Restore LargeTitleHeaderSearchContentContainer in packages/ui/src/ (provides opaque bg + SafeAreaView) - Restore LargeTitleHeaderOverlapFixIOS in packages/ui/src/ (provides iOS header overlap fix) - Fix dashboard empty-state text: revert t('dashboard.searchPrompt') back to t('dashboard.searchPlaceholder') = "Search dashboard" - Remove incorrectly added dashboard.searchPrompt translation key from en.json - Update all 6 screen imports to use @packrat/ui/src/search-overlay - Update packages/ui/tsconfig.json to include src/**/* - Correct migration doc: components should be moved, not deleted --- apps/expo/app/(app)/(tabs)/(home)/index.tsx | 4 ++-- .../catalog/screens/CatalogItemsScreen.tsx | 2 +- .../features/guides/screens/GuidesListScreen.tsx | 2 +- .../screens/PackTemplateListScreen.tsx | 2 +- .../features/packs/screens/PackListScreen.tsx | 2 +- .../features/trips/screens/TripListScreen.tsx | 2 +- apps/expo/lib/i18n/locales/en.json | 3 +-- docs/migrations/nativewindui-to-expo-ui.md | 6 +++--- .../ui/src/large-title-header-overlap-fix-ios.tsx | 15 +++++++++++++++ ...arge-title-header-search-content-container.tsx | 9 +++++++++ .../ui/src/search-overlay}/SearchOverlay.ios.tsx | 6 +++--- .../ui/src/search-overlay}/SearchOverlay.tsx | 3 ++- .../ui/src/search-overlay}/index.ts | 0 .../ui/src/search-overlay}/types.ts | 0 packages/ui/tsconfig.json | 2 +- 15 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 packages/ui/src/large-title-header-overlap-fix-ios.tsx create mode 100644 packages/ui/src/large-title-header-search-content-container.tsx rename {apps/expo/components/SearchOverlay => packages/ui/src/search-overlay}/SearchOverlay.ios.tsx (86%) rename {apps/expo/components/SearchOverlay => packages/ui/src/search-overlay}/SearchOverlay.tsx (97%) rename {apps/expo/components/SearchOverlay => packages/ui/src/search-overlay}/index.ts (100%) rename {apps/expo/components/SearchOverlay => packages/ui/src/search-overlay}/types.ts (100%) diff --git a/apps/expo/app/(app)/(tabs)/(home)/index.tsx b/apps/expo/app/(app)/(tabs)/(home)/index.tsx index 96d482f7a1..886206e0f3 100644 --- a/apps/expo/app/(app)/(tabs)/(home)/index.tsx +++ b/apps/expo/app/(app)/(tabs)/(home)/index.tsx @@ -4,9 +4,9 @@ import type { BottomSheetModal } from '@gorhom/bottom-sheet'; import { arrayIncludes, assertIsString, objectKeys } from '@packrat/guards'; import type { ListDataItem } from '@packrat/ui/nativewindui'; import { List, type ListRenderItemInfo, ListSectionHeader } from '@packrat/ui/nativewindui'; +import { SearchOverlay } from '@packrat/ui/src/search-overlay'; import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; import { Icon } from 'expo-app/components/Icon'; -import { SearchOverlay } from 'expo-app/components/SearchOverlay'; import { appConfig, featureFlags } from 'expo-app/config'; import { AIChatTile } from 'expo-app/features/ai/components/AIChatTile'; import { ReportedContentTile } from 'expo-app/features/ai/components/ReportedContentTile'; @@ -301,7 +301,7 @@ export default function DashboardScreen() { - {t('dashboard.searchPrompt')} + {t('dashboard.searchPlaceholder')} ) diff --git a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx index 2b3184b9d6..9fb6720d7d 100644 --- a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx +++ b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx @@ -1,10 +1,10 @@ import { Text } from '@packrat/ui/nativewindui'; +import { SearchOverlay } from '@packrat/ui/src/search-overlay'; import { catalogGroupVariantsAtom } from 'expo-app/atoms/catalogGroupAtom'; import { searchValueAtom } from 'expo-app/atoms/itemListAtoms'; import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; import { CategoriesFilter } from 'expo-app/components/CategoriesFilter'; import { Icon } from 'expo-app/components/Icon'; -import { SearchOverlay } from 'expo-app/components/SearchOverlay'; import { withAuthWall } from 'expo-app/features/auth/hocs'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; diff --git a/apps/expo/features/guides/screens/GuidesListScreen.tsx b/apps/expo/features/guides/screens/GuidesListScreen.tsx index 0a58c6c96a..3db3a1584f 100644 --- a/apps/expo/features/guides/screens/GuidesListScreen.tsx +++ b/apps/expo/features/guides/screens/GuidesListScreen.tsx @@ -1,6 +1,6 @@ import { Text } from '@packrat/ui/nativewindui'; +import { SearchOverlay } from '@packrat/ui/src/search-overlay'; import { CategoriesFilter } from 'expo-app/components/CategoriesFilter'; -import { SearchOverlay } from 'expo-app/components/SearchOverlay'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { Stack, useRouter } from 'expo-router'; diff --git a/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx b/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx index f30b909d5f..2d83a41ca5 100644 --- a/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx +++ b/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx @@ -1,7 +1,7 @@ import type { BottomSheetModal } from '@gorhom/bottom-sheet'; import { SegmentedControl } from '@packrat/ui/nativewindui'; +import { SearchOverlay } from '@packrat/ui/src/search-overlay'; import { Icon } from 'expo-app/components/Icon'; -import { SearchOverlay } from 'expo-app/components/SearchOverlay'; import { useAuth } from 'expo-app/features/auth/hooks/useAuth'; import { useUser } from 'expo-app/features/auth/hooks/useUser'; import type { PackCategory } from 'expo-app/features/packs/types'; diff --git a/apps/expo/features/packs/screens/PackListScreen.tsx b/apps/expo/features/packs/screens/PackListScreen.tsx index 8e7d0046ef..993e25f923 100644 --- a/apps/expo/features/packs/screens/PackListScreen.tsx +++ b/apps/expo/features/packs/screens/PackListScreen.tsx @@ -1,7 +1,7 @@ import { ActivityIndicator, Button, SegmentedControl } from '@packrat/ui/nativewindui'; +import { SearchOverlay } from '@packrat/ui/src/search-overlay'; import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; import { Icon } from 'expo-app/components/Icon'; -import { SearchOverlay } from 'expo-app/components/SearchOverlay'; import { useAuth } from 'expo-app/features/auth/hooks/useAuth'; import { PackCard } from 'expo-app/features/packs/components/PackCard'; import { SearchResults } from 'expo-app/features/packs/components/SearchResults'; diff --git a/apps/expo/features/trips/screens/TripListScreen.tsx b/apps/expo/features/trips/screens/TripListScreen.tsx index 1db00255cb..b605c23739 100644 --- a/apps/expo/features/trips/screens/TripListScreen.tsx +++ b/apps/expo/features/trips/screens/TripListScreen.tsx @@ -1,6 +1,6 @@ +import { SearchOverlay } from '@packrat/ui/src/search-overlay'; import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; import { Icon } from 'expo-app/components/Icon'; -import { SearchOverlay } from 'expo-app/components/SearchOverlay'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { testIds } from 'expo-app/lib/testIds'; diff --git a/apps/expo/lib/i18n/locales/en.json b/apps/expo/lib/i18n/locales/en.json index 4e89642650..8b78038367 100644 --- a/apps/expo/lib/i18n/locales/en.json +++ b/apps/expo/lib/i18n/locales/en.json @@ -217,8 +217,7 @@ "title": "Dashboard", "searchPlaceholder": "Search dashboard", "noResults": "No matching tiles found", - "tryDifferent": "Try different keywords or clear your search", - "searchPrompt": "Type to find tiles on your dashboard" + "tryDifferent": "Try different keywords or clear your search" }, "packs": { "currentPack": "Current Pack", diff --git a/docs/migrations/nativewindui-to-expo-ui.md b/docs/migrations/nativewindui-to-expo-ui.md index ece8e85d28..1711c45b28 100644 --- a/docs/migrations/nativewindui-to-expo-ui.md +++ b/docs/migrations/nativewindui-to-expo-ui.md @@ -70,8 +70,8 @@ Everything here is either replaced by `@expo/ui` via `packages/ui` or moved to a | `ScreenContent.tsx` | Move to `packages/ui/src/screen-content.tsx` | | `Markdown.tsx` | Move to `packages/ui/src/markdown.tsx` | | `Icon/` | Move to `packages/ui/src/icon/` — wraps `@expo/ui` Universal `Icon` | -| `LargeTitleHeaderOverlapFixIOS.tsx` | Delete — workaround no longer needed after Phase 2 | -| `LargeTitleHeaderSearchContentContainer.tsx` | Delete — same | +| `LargeTitleHeaderOverlapFixIOS.tsx` | Move to `packages/ui/src/large-title-header-overlap-fix-ios.tsx` — still needed | +| `LargeTitleHeaderSearchContentContainer.tsx` | Move to `packages/ui/src/large-title-header-search-content-container.tsx` — still needed | | `AndroidTabBarInsetFix.tsx` | Move to `packages/ui/src/android-tab-bar-inset-fix.android.tsx` | | `BackButton.tsx` | Move to `packages/ui/src/back-button.tsx` | | `HeaderButton.tsx` | Move to `packages/ui/src/header-button.tsx` | @@ -97,7 +97,7 @@ Replace ref-based imperative navigation APIs. - `LargeTitleHeader` → restructure each screen to use `Stack.Screen.Title`, `Stack.SearchBar`, `Stack.Toolbar`. Each screen group (home, packs, catalog, trips, profile) is one sub-PR. - `Sheet` + `useSheetRef` → replace `ref.current.present()` calls with `router.push('/sheet-route')` + `presentation: 'formSheet'` in the Stack layout. New route files replace old modal components. - `SearchInput` / `AdaptiveSearchHeader` → `Stack.SearchBar` or `headerSearchBarOptions`. -- Delete `LargeTitleHeaderOverlapFixIOS.tsx` and `LargeTitleHeaderSearchContentContainer.tsx` once their screens are migrated. +- `LargeTitleHeaderOverlapFixIOS` and `LargeTitleHeaderSearchContentContainer` are moved to `packages/ui/src/` and remain available for screens that need them. Estimated effort: 3–5 days. One PR per tab section. diff --git a/packages/ui/src/large-title-header-overlap-fix-ios.tsx b/packages/ui/src/large-title-header-overlap-fix-ios.tsx new file mode 100644 index 0000000000..a18587d9d2 --- /dev/null +++ b/packages/ui/src/large-title-header-overlap-fix-ios.tsx @@ -0,0 +1,15 @@ +import type { ReactNode } from 'react'; +import { Platform, SafeAreaView, View } from 'react-native'; + +export function LargeTitleHeaderOverlapFixIOS({ children }: { children?: ReactNode }) { + if (Platform.OS === 'android') { + if (!children) return null; + return <>{children}; + } + return ( + + {children} + {!children && } + + ); +} diff --git a/packages/ui/src/large-title-header-search-content-container.tsx b/packages/ui/src/large-title-header-search-content-container.tsx new file mode 100644 index 0000000000..48721fe9b5 --- /dev/null +++ b/packages/ui/src/large-title-header-search-content-container.tsx @@ -0,0 +1,9 @@ +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; +import type { ReactNode } from 'react'; +import { Platform, SafeAreaView, View } from 'react-native'; + +export function LargeTitleHeaderSearchContentContainer({ children }: { children: ReactNode }) { + const { colors } = useColorScheme(); + const Container = Platform.OS === 'ios' ? SafeAreaView : View; + return {children}; +} diff --git a/apps/expo/components/SearchOverlay/SearchOverlay.ios.tsx b/packages/ui/src/search-overlay/SearchOverlay.ios.tsx similarity index 86% rename from apps/expo/components/SearchOverlay/SearchOverlay.ios.tsx rename to packages/ui/src/search-overlay/SearchOverlay.ios.tsx index bf3a543167..d9f9c2d659 100644 --- a/apps/expo/components/SearchOverlay/SearchOverlay.ios.tsx +++ b/packages/ui/src/search-overlay/SearchOverlay.ios.tsx @@ -1,6 +1,6 @@ import { Stack } from 'expo-router'; import { useState } from 'react'; -import { StyleSheet, View } from 'react-native'; +import { StyleSheet } from 'react-native'; import Animated, { FadeIn } from 'react-native-reanimated'; import type { SearchOverlayProps } from './types'; @@ -25,9 +25,9 @@ export function SearchOverlay({ placeholder, value, onChangeText, children }: Se - {children} + {children} )} diff --git a/apps/expo/components/SearchOverlay/SearchOverlay.tsx b/packages/ui/src/search-overlay/SearchOverlay.tsx similarity index 97% rename from apps/expo/components/SearchOverlay/SearchOverlay.tsx rename to packages/ui/src/search-overlay/SearchOverlay.tsx index 4380914271..df2d0c7002 100644 --- a/apps/expo/components/SearchOverlay/SearchOverlay.tsx +++ b/packages/ui/src/search-overlay/SearchOverlay.tsx @@ -61,7 +61,7 @@ export function SearchOverlay({ /> {isOpen && ( - + Date: Mon, 15 Jun 2026 10:20:31 +0100 Subject: [PATCH 06/17] fix(migration): absorb SearchContentContainer into overlays, restore LargeTitleHeaderOverlapFixIOS usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Absorb LargeTitleHeaderSearchContentContainer behavior into SearchOverlay.ios.tsx: wrap children in SafeAreaView className="flex-1 bg-background" so the iOS overlay is opaque and content respects safe area insets — matches original container behavior per platform - Delete LargeTitleHeaderSearchContentContainer (no longer needed as standalone component) - Restore LargeTitleHeaderOverlapFixIOS usage in all 4 pre-migration screens: - CatalogItemsScreen: add as first item in listHeader - GuidesListScreen: same pattern - PackListScreen: wrap FlatList with - LocationsScreen: wrap all visual content with - Update migration doc to reflect SearchContentContainer is absorbed, not moved --- .../catalog/screens/CatalogItemsScreen.tsx | 2 + .../guides/screens/GuidesListScreen.tsx | 2 + .../features/packs/screens/PackListScreen.tsx | 161 ++++++------- .../weather/screens/LocationsScreen.tsx | 217 +++++++++--------- docs/migrations/nativewindui-to-expo-ui.md | 2 +- ...-title-header-search-content-container.tsx | 9 - .../src/search-overlay/SearchOverlay.ios.tsx | 4 +- 7 files changed, 199 insertions(+), 198 deletions(-) delete mode 100644 packages/ui/src/large-title-header-search-content-container.tsx diff --git a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx index 9fb6720d7d..03238b9e37 100644 --- a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx +++ b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx @@ -1,4 +1,5 @@ import { Text } from '@packrat/ui/nativewindui'; +import { LargeTitleHeaderOverlapFixIOS } from '@packrat/ui/src/large-title-header-overlap-fix-ios'; import { SearchOverlay } from '@packrat/ui/src/search-overlay'; import { catalogGroupVariantsAtom } from 'expo-app/atoms/catalogGroupAtom'; import { searchValueAtom } from 'expo-app/atoms/itemListAtoms'; @@ -104,6 +105,7 @@ function CatalogItemsScreen() { return ( <> + { return ( <> + - pack.id} - stickyHeaderIndices={[0]} - contentInsetAdjustmentBehavior="automatic" - renderItem={({ item: pack }) => ( - - - - )} - refreshControl={ - selectedTypeIndex === ALL_PACKS_INDEX ? ( - - ) : undefined - } - ListHeaderComponent={ - - {!isAuthenticated && } - {isAuthenticated && ( - - { - setSelectedTypeIndex(index); - }} - /> + + pack.id} + stickyHeaderIndices={[0]} + contentInsetAdjustmentBehavior="automatic" + renderItem={({ item: pack }) => ( + + + + )} + refreshControl={ + selectedTypeIndex === ALL_PACKS_INDEX ? ( + + ) : undefined + } + ListHeaderComponent={ + + {!isAuthenticated && } + {isAuthenticated && ( + + { + setSelectedTypeIndex(index); + }} + /> + + )} + + + {filterOptions.map(renderFilterChip)} + - )} - - - {filterOptions.map(renderFilterChip)} - + {selectedTypeIndex === USER_PACKS_INDEX && ( + + + {filteredPacks?.length || 0} {filteredPacks?.length === 1 ? 'pack' : 'packs'} + + + )} - {selectedTypeIndex === USER_PACKS_INDEX && ( - - - {filteredPacks?.length || 0} {filteredPacks?.length === 1 ? 'pack' : 'packs'} + } + ListEmptyComponent={ + selectedTypeIndex === ALL_PACKS_INDEX ? ( + renderAllPacksEmptyState() + ) : ( + + + + + + {t('packs.noPacksFound')} - - )} - - } - ListEmptyComponent={ - selectedTypeIndex === ALL_PACKS_INDEX ? ( - renderAllPacksEmptyState() - ) : ( - - - - - - {t('packs.noPacksFound')} - - - {activeFilter === 'all' - ? "You haven't created or found any public packs yet." - : `You don't have any ${activeFilter} packs.`} - - - - {t('packs.createNewPack')} + + {activeFilter === 'all' + ? "You haven't created or found any public packs yet." + : `You don't have any ${activeFilter} packs.`} - - - ) - } - ListFooterComponent={} - contentContainerStyle={{ flexGrow: 1 }} - /> + + + {t('packs.createNewPack')} + + + + ) + } + ListFooterComponent={} + contentContainerStyle={{ flexGrow: 1 }} + /> + ); } diff --git a/apps/expo/features/weather/screens/LocationsScreen.tsx b/apps/expo/features/weather/screens/LocationsScreen.tsx index ad3074b04b..a62e029e80 100644 --- a/apps/expo/features/weather/screens/LocationsScreen.tsx +++ b/apps/expo/features/weather/screens/LocationsScreen.tsx @@ -1,4 +1,5 @@ import { Button, Text } from '@packrat/ui/nativewindui'; +import { LargeTitleHeaderOverlapFixIOS } from '@packrat/ui/src/large-title-header-overlap-fix-ios'; import { Icon } from 'expo-app/components/Icon'; import { SearchInput } from 'expo-app/components/SearchInput'; import { withAuthWall } from 'expo-app/features/auth/hocs'; @@ -130,123 +131,125 @@ function LocationsScreen() { }} /> - - setIsSearchFocused(true)} - onBlur={() => { - // Only unfocus if search is empty - if (searchQuery.length === 0) { - setIsSearchFocused(false); - } - }} - /> - - - {showNoSearchResults && ( - - - {t('weather.searchResults')} - - - - - {t('weather.noLocationsMatch', { query: searchQuery })} + + + setIsSearchFocused(true)} + onBlur={() => { + // Only unfocus if search is empty + if (searchQuery.length === 0) { + setIsSearchFocused(false); + } + }} + /> + + + {showNoSearchResults && ( + + + {t('weather.searchResults')} - - - {t('weather.clearSearch')} - - - {t('weather.addNewLocation')} - + + + + {t('weather.noLocationsMatch', { query: searchQuery })} + + + + {t('weather.clearSearch')} + + + {t('weather.addNewLocation')} + + + + )} + + {isLoading ? ( + + + {t('weather.loadingWeatherData')} - - )} + ) : ( + + } + keyboardShouldPersistTaps="handled" + > + {showLocationsList && ( + <> + {showSearchResults && ( + + + {filteredLocations.length}{' '} + {filteredLocations.length === 1 ? t('weather.result') : t('weather.results')} + + + )} - {isLoading ? ( - - - {t('weather.loadingWeatherData')} - - ) : ( - - } - keyboardShouldPersistTaps="handled" - > - {showLocationsList && ( - <> - {showSearchResults && ( - - {filteredLocations.length}{' '} - {filteredLocations.length === 1 ? t('weather.result') : t('weather.results')} + + {t('weather.longPressForOptions')} - )} - - - {t('weather.longPressForOptions')} + {filteredLocations.map((location) => ( + handleLocationPress(location.id)} + onSetActive={() => handleSetActive(location.id)} + onRemove={() => handleRemoveLocation(location.id)} + /> + ))} + + )} + + {showEmptyState && ( + + + + {t('weather.noSavedLocations')} + + {t('weather.noSavedLocationsDesc')} + + - - {filteredLocations.map((location) => ( - handleLocationPress(location.id)} - onSetActive={() => handleSetActive(location.id)} - onRemove={() => handleRemoveLocation(location.id)} - /> - ))} - - )} - - {showEmptyState && ( - - - - {t('weather.noSavedLocations')} - - - {t('weather.noSavedLocationsDesc')} - - - - )} - - )} + )} + + )} + ); } diff --git a/docs/migrations/nativewindui-to-expo-ui.md b/docs/migrations/nativewindui-to-expo-ui.md index 1711c45b28..75665a8189 100644 --- a/docs/migrations/nativewindui-to-expo-ui.md +++ b/docs/migrations/nativewindui-to-expo-ui.md @@ -71,7 +71,7 @@ Everything here is either replaced by `@expo/ui` via `packages/ui` or moved to a | `Markdown.tsx` | Move to `packages/ui/src/markdown.tsx` | | `Icon/` | Move to `packages/ui/src/icon/` — wraps `@expo/ui` Universal `Icon` | | `LargeTitleHeaderOverlapFixIOS.tsx` | Move to `packages/ui/src/large-title-header-overlap-fix-ios.tsx` — still needed | -| `LargeTitleHeaderSearchContentContainer.tsx` | Move to `packages/ui/src/large-title-header-search-content-container.tsx` — still needed | +| `LargeTitleHeaderSearchContentContainer.tsx` | Absorbed into platform SearchOverlay components — delete | | `AndroidTabBarInsetFix.tsx` | Move to `packages/ui/src/android-tab-bar-inset-fix.android.tsx` | | `BackButton.tsx` | Move to `packages/ui/src/back-button.tsx` | | `HeaderButton.tsx` | Move to `packages/ui/src/header-button.tsx` | diff --git a/packages/ui/src/large-title-header-search-content-container.tsx b/packages/ui/src/large-title-header-search-content-container.tsx deleted file mode 100644 index 48721fe9b5..0000000000 --- a/packages/ui/src/large-title-header-search-content-container.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; -import type { ReactNode } from 'react'; -import { Platform, SafeAreaView, View } from 'react-native'; - -export function LargeTitleHeaderSearchContentContainer({ children }: { children: ReactNode }) { - const { colors } = useColorScheme(); - const Container = Platform.OS === 'ios' ? SafeAreaView : View; - return {children}; -} diff --git a/packages/ui/src/search-overlay/SearchOverlay.ios.tsx b/packages/ui/src/search-overlay/SearchOverlay.ios.tsx index d9f9c2d659..70b3c492a6 100644 --- a/packages/ui/src/search-overlay/SearchOverlay.ios.tsx +++ b/packages/ui/src/search-overlay/SearchOverlay.ios.tsx @@ -1,6 +1,6 @@ import { Stack } from 'expo-router'; import { useState } from 'react'; -import { StyleSheet } from 'react-native'; +import { SafeAreaView, StyleSheet } from 'react-native'; import Animated, { FadeIn } from 'react-native-reanimated'; import type { SearchOverlayProps } from './types'; @@ -27,7 +27,7 @@ export function SearchOverlay({ placeholder, value, onChangeText, children }: Se style={StyleSheet.absoluteFill} className="bg-background z-50" > - {children} + {children} )} From 4980d59dd0235bea900bce1c8fff276d8653ceab Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 15 Jun 2026 11:31:00 +0100 Subject: [PATCH 07/17] fix(migration): improve search overlay UX and empty states - Show prompt text before search starts in CatalogItemsScreen - Fix empty state logic order (loading > no results > prompt) - Add flex-1 to SearchResults empty view for proper centering - Fix SafeAreaView styling in SearchOverlay.ios to use style prop --- .../features/catalog/screens/CatalogItemsScreen.tsx | 12 ++++++------ .../expo/features/packs/components/SearchResults.tsx | 2 +- packages/ui/src/search-overlay/SearchOverlay.ios.tsx | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx index 03238b9e37..f958758d29 100644 --- a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx +++ b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx @@ -133,7 +133,11 @@ function CatalogItemsScreen() { value={searchValue} onChangeText={setSearchValue} > - {isVectorLoading || !isQueryReady ? ( + {!isSearching ? ( + + {t('catalog.searchCatalog')} + + ) : isVectorLoading || !isQueryReady ? ( @@ -167,7 +171,7 @@ function CatalogItemsScreen() { {t('catalog.unableToSearch')} - ) : isSearching ? ( + ) : ( <> @@ -179,10 +183,6 @@ function CatalogItemsScreen() { {t('catalog.tryAdjustingFilters')} - ) : ( - - {t('catalog.searchPlaceholder')} - )} )} diff --git a/apps/expo/features/packs/components/SearchResults.tsx b/apps/expo/features/packs/components/SearchResults.tsx index 5931267892..ae24878cdf 100644 --- a/apps/expo/features/packs/components/SearchResults.tsx +++ b/apps/expo/features/packs/components/SearchResults.tsx @@ -33,7 +33,7 @@ export function SearchResults({ results, searchValue, onResultPress }: SearchRes )} /> ) : ( - + No packs found for "{searchValue}" ); diff --git a/packages/ui/src/search-overlay/SearchOverlay.ios.tsx b/packages/ui/src/search-overlay/SearchOverlay.ios.tsx index 70b3c492a6..6067b4820c 100644 --- a/packages/ui/src/search-overlay/SearchOverlay.ios.tsx +++ b/packages/ui/src/search-overlay/SearchOverlay.ios.tsx @@ -27,7 +27,7 @@ export function SearchOverlay({ placeholder, value, onChangeText, children }: Se style={StyleSheet.absoluteFill} className="bg-background z-50" > - {children} + {children} )} From 3c0cc70ad99f010398363f850d49e228b1117cd2 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 15 Jun 2026 11:43:42 +0100 Subject: [PATCH 08/17] fix(types): re-export missing nativewindui components and fix useColorScheme import Add Checkbox, ContextMenu, createContextItem, DropdownMenu, createDropdownItem, Toolbar, and ToolbarCTA re-exports to packages/ui/nativewindui/index.ts that were missing from the migration tracker. Fix ToolCard.tsx to import useColorScheme from its migrated location (expo-app/lib/hooks/useColorScheme) per Phase 1. --- apps/expo/features/ai/components/ToolCard.tsx | 9 ++------- packages/ui/nativewindui/index.ts | 6 +++++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/expo/features/ai/components/ToolCard.tsx b/apps/expo/features/ai/components/ToolCard.tsx index ac3b4f2a8d..5e9d10323a 100644 --- a/apps/expo/features/ai/components/ToolCard.tsx +++ b/apps/expo/features/ai/components/ToolCard.tsx @@ -1,11 +1,6 @@ import { EvilIcons, Ionicons } from '@expo/vector-icons'; -import { - ActivityIndicator, - Card, - CardContent, - Text, - useColorScheme, -} from '@packrat/ui/nativewindui'; +import { ActivityIndicator, Card, CardContent, Text } from '@packrat/ui/nativewindui'; +import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import type React from 'react'; import { Pressable, View } from 'react-native'; diff --git a/packages/ui/nativewindui/index.ts b/packages/ui/nativewindui/index.ts index fb134d2ffb..e4845f205f 100644 --- a/packages/ui/nativewindui/index.ts +++ b/packages/ui/nativewindui/index.ts @@ -50,7 +50,11 @@ export { CardSubtitle, } from '@packrat-ai/nativewindui'; // 8 uses → JC Card (Android) + custom View (iOS) export { SegmentedControl } from '@packrat-ai/nativewindui'; // 3 uses → @expo/ui community SegmentedControl -export type { ContextMenuMethods } from '@packrat-ai/nativewindui'; // 1 use → SwiftUI ContextMenu + JC DropdownMenu +export { Checkbox } from '@packrat-ai/nativewindui'; // 3 uses → @expo/ui Universal Checkbox +export { ContextMenu, createContextItem, createContextSubMenu } from '@packrat-ai/nativewindui'; // multiple uses → SwiftUI ContextMenu + JC DropdownMenu +export type { ContextMenuMethods } from '@packrat-ai/nativewindui'; +export { DropdownMenu, createDropdownItem, createDropdownSubMenu } from '@packrat-ai/nativewindui'; // multiple uses → @expo/ui DropdownMenu +export { Toolbar, ToolbarCTA, ToolbarIcon } from '@packrat-ai/nativewindui'; // multiple uses → platform-specific Toolbar // // Phase 5 — no @expo/ui equivalent export { Avatar, AvatarFallback, AvatarImage } from '@packrat-ai/nativewindui'; // 6 uses → @rn-primitives/avatar From 3d5921a42a194e3a5ed0f8a0c9b60379a57f4584 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 15 Jun 2026 15:29:02 +0100 Subject: [PATCH 09/17] fix(migration): restore weather header and SearchInput border, remove stale headerShown:false - Remove headerShown:false override for weather/index so Stack.Screen with headerLargeTitle:true takes effect in LocationsScreen - Restore containerClassName="border border-border" on SearchInput in LocationsScreen and LocationSearchScreen - Wrap Stack.Screen inside LargeTitleHeaderOverlapFixIOS to preserve overlap fix --- apps/expo/app/(app)/(tabs)/(home)/index.tsx | 80 +++++++++---------- apps/expo/app/(app)/_layout.tsx | 1 - apps/expo/components/SearchInput.tsx | 13 ++- .../weather/screens/LocationSearchScreen.tsx | 1 + .../weather/screens/LocationsScreen.tsx | 25 +++--- packages/ui/nativewindui/index.ts | 6 +- 6 files changed, 63 insertions(+), 63 deletions(-) diff --git a/apps/expo/app/(app)/(tabs)/(home)/index.tsx b/apps/expo/app/(app)/(tabs)/(home)/index.tsx index 886206e0f3..4e3f9cfaa3 100644 --- a/apps/expo/app/(app)/(tabs)/(home)/index.tsx +++ b/apps/expo/app/(app)/(tabs)/(home)/index.tsx @@ -254,38 +254,38 @@ export default function DashboardScreen() { value={searchValue} onChangeText={setSearchValue} > - { - assertIsString(item); - if (!item.startsWith(DASHBOARD_GAP_PREFIX) && arrayIncludes(TILE_NAMES, item)) { - const Component = tileInfo[item].component; - return ( - setSearchValue('')} - > - - - ); + {searchValue ? ( + { + assertIsString(item); + if (!item.startsWith(DASHBOARD_GAP_PREFIX) && arrayIncludes(TILE_NAMES, item)) { + const Component = tileInfo[item].component; + return ( + setSearchValue('')} + > + + + ); + } + return null; + }} + ListHeaderComponent={() => + filteredTiles.length > 0 ? ( + + {filteredTiles.length}{' '} + {filteredTiles.length === 1 + ? appConfig.dashboard.strings.resultSingular + : appConfig.dashboard.strings.resultPlural} + + ) : null } - return null; - }} - ListHeaderComponent={() => - filteredTiles.length > 0 ? ( - - {filteredTiles.length}{' '} - {filteredTiles.length === 1 - ? appConfig.dashboard.strings.resultSingular - : appConfig.dashboard.strings.resultPlural} - - ) : null - } - ListEmptyComponent={() => - searchValue.trim() ? ( + ListEmptyComponent={() => ( @@ -296,17 +296,13 @@ export default function DashboardScreen() { {t('dashboard.tryDifferent')} - ) : ( - - - - - {t('dashboard.searchPlaceholder')} - - - ) - } - /> + )} + /> + ) : ( + + {t('dashboard.searchPlaceholder')} + + )} - , - React.ComponentPropsWithoutRef + React.ComponentRef, + React.ComponentProps >((props, ref) => { - const searchInputRef = useRef>(null); + const searchInputRef = useRef>(null); // Apply keyboard hide blur fix useKeyboardHideBlur({ textInputRef: asNonNullableRef(searchInputRef) }); @@ -24,7 +23,7 @@ export const SearchInput = forwardRef< return searchInputRef.current; }, []); - return ; + return ; }); SearchInput.displayName = 'SearchInput'; diff --git a/apps/expo/features/weather/screens/LocationSearchScreen.tsx b/apps/expo/features/weather/screens/LocationSearchScreen.tsx index b7c52f422e..c7bbad5872 100644 --- a/apps/expo/features/weather/screens/LocationSearchScreen.tsx +++ b/apps/expo/features/weather/screens/LocationSearchScreen.tsx @@ -411,6 +411,7 @@ export default function LocationSearchScreen() { placeholder={t('weather.searchForCity')} value={query} onChangeText={handleSearchChange} + containerClassName="border border-border" clearButtonMode="while-editing" /> diff --git a/apps/expo/features/weather/screens/LocationsScreen.tsx b/apps/expo/features/weather/screens/LocationsScreen.tsx index a62e029e80..2558688aad 100644 --- a/apps/expo/features/weather/screens/LocationsScreen.tsx +++ b/apps/expo/features/weather/screens/LocationsScreen.tsx @@ -119,25 +119,26 @@ function LocationsScreen() { return ( - ( - - - - ), - }} - /> - + ( + + + + ), + }} + /> + setIsSearchFocused(true)} onBlur={() => { // Only unfocus if search is empty diff --git a/packages/ui/nativewindui/index.ts b/packages/ui/nativewindui/index.ts index e4845f205f..fae00c3b2c 100644 --- a/packages/ui/nativewindui/index.ts +++ b/packages/ui/nativewindui/index.ts @@ -9,7 +9,11 @@ // Full plan: docs/migrations/nativewindui-to-expo-ui.md // // Phase 1 ✓ done — useColorScheme → expo-app/lib/hooks/useColorScheme, cn → expo-app/lib/cn -// Phase 2 ✓ done — LargeTitleHeader/SearchInput → Stack.Screen + headerSearchBarOptions +// Phase 2 — LargeTitleHeader/SearchInput → Stack.Screen + headerSearchBarOptions +// LargeTitleHeader ✓ done +// SearchInput — reverted, pending re-migration +export { SearchInput } from '@packrat-ai/nativewindui'; // uses → headerSearchBarOptions +export type { SearchInputProps, SearchInputRef } from '@packrat-ai/nativewindui'; // // Phase 3 — @expo/ui Universal → packages/ui/src/ export { Text, TextClassContext, textVariants } from '@packrat-ai/nativewindui'; // 114 uses → @expo/ui Universal Text From c8e703d00949a459ce54a19b63aebee9e6f27d98 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 15 Jun 2026 19:04:54 +0100 Subject: [PATCH 10/17] feat(app-bar): implement Android large title header across all screens Adds a custom MD3-style Large Top App Bar for Android using a fixed two-row layout (action row + large title) rendered via the Stack.Screen `header:` prop. iOS continues to use native `headerLargeTitle: true`. - AppBarAndroid: static large title (32sp/400) below the action row, no collapse animation - Removed useAppBarScroll and AppBarLargeTitle (no longer needed) - Migrated all 27+ screens from headerLargeTitle to getAppBarOptions() --- apps/expo/app/(app)/(tabs)/(home)/index.tsx | 4 +- apps/expo/app/(app)/(tabs)/profile/index.tsx | 3 +- apps/expo/app/(app)/_layout.tsx | 5 +- apps/expo/app/(app)/current-pack/[id].tsx | 3 +- apps/expo/app/(app)/demo/index.tsx | 3 +- apps/expo/app/(app)/gear-inventory.tsx | 3 +- .../expo/app/(app)/messages/conversations.tsx | 3 +- apps/expo/app/(app)/pack-categories/[id].tsx | 3 +- apps/expo/app/(app)/pack-stats/[id].tsx | 3 +- apps/expo/app/(app)/recent-packs.tsx | 3 +- .../app/(app)/season-suggestions-results.tsx | 3 +- apps/expo/app/(app)/season-suggestions.tsx | 3 +- apps/expo/app/(app)/shared-packs.tsx | 3 +- apps/expo/app/(app)/shopping-list.tsx | 3 +- apps/expo/app/(app)/trail-conditions.tsx | 3 +- .../app/(app)/weather-alert-preferences.tsx | 3 +- apps/expo/app/(app)/weather-alerts.tsx | 3 +- apps/expo/app/(app)/weight-analysis/[id].tsx | 3 +- .../ai-packs/screens/AIPacksScreen.tsx | 4 +- .../ai/screens/ReportedContentScreen.tsx | 3 +- .../catalog/screens/CatalogItemsScreen.tsx | 3 +- .../expo/features/feed/screens/FeedScreen.tsx | 3 +- .../guides/screens/GuidesListScreen.tsx | 3 +- .../screens/PackTemplateListScreen.tsx | 3 +- .../features/packs/screens/PackListScreen.tsx | 3 +- .../features/trips/screens/TripListScreen.tsx | 3 +- .../weather/screens/LocationsScreen.tsx | 8 +-- .../wildlife/screens/WildlifeScreen.tsx | 3 +- packages/ui/src/app-bar/AppBarAndroid.tsx | 67 +++++++++++++++++++ packages/ui/src/app-bar/index.tsx | 26 +++++++ .../ui/src/search-overlay/SearchOverlay.tsx | 5 +- 31 files changed, 155 insertions(+), 36 deletions(-) create mode 100644 packages/ui/src/app-bar/AppBarAndroid.tsx create mode 100644 packages/ui/src/app-bar/index.tsx diff --git a/apps/expo/app/(app)/(tabs)/(home)/index.tsx b/apps/expo/app/(app)/(tabs)/(home)/index.tsx index 4e3f9cfaa3..7f3ff93736 100644 --- a/apps/expo/app/(app)/(tabs)/(home)/index.tsx +++ b/apps/expo/app/(app)/(tabs)/(home)/index.tsx @@ -4,6 +4,7 @@ import type { BottomSheetModal } from '@gorhom/bottom-sheet'; import { arrayIncludes, assertIsString, objectKeys } from '@packrat/guards'; import type { ListDataItem } from '@packrat/ui/nativewindui'; import { List, type ListRenderItemInfo, ListSectionHeader } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { SearchOverlay } from '@packrat/ui/src/search-overlay'; import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; import { Icon } from 'expo-app/components/Icon'; @@ -165,7 +166,6 @@ export default function DashboardScreen() { const isFocused = useIsFocused(); const { hasMinimumItems } = useHasMinimumInventory(20); const { announcementSeen } = useSeasonSuggestionsPrefs(); - useEffect(() => { if (!isFocused || !hasMinimumItems || announcementSeen) return; const timer = setTimeout(() => { @@ -244,8 +244,8 @@ export default function DashboardScreen() { <> diff --git a/apps/expo/app/(app)/(tabs)/profile/index.tsx b/apps/expo/app/(app)/(tabs)/profile/index.tsx index c7769d0770..96909de7c4 100644 --- a/apps/expo/app/(app)/(tabs)/profile/index.tsx +++ b/apps/expo/app/(app)/(tabs)/profile/index.tsx @@ -12,6 +12,7 @@ import { ListSectionHeader, Text, } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import AsyncStorage from '@react-native-async-storage/async-storage'; import * as Sentry from '@sentry/react-native'; import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; @@ -80,8 +81,8 @@ function Profile() { const { t } = useTranslation(); const SCREEN_OPTIONS = { + ...getAppBarOptions(), title: t('profile.profile'), - headerLargeTitle: true, headerBackVisible: false, headerRight: () => ( diff --git a/apps/expo/app/(app)/_layout.tsx b/apps/expo/app/(app)/_layout.tsx index 97b920e81f..191605bdf3 100644 --- a/apps/expo/app/(app)/_layout.tsx +++ b/apps/expo/app/(app)/_layout.tsx @@ -18,6 +18,7 @@ import { getTripDetailOptions } from 'expo-app/features/trips/utils/getTripDetai import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import type { TranslationFunction } from 'expo-app/lib/i18n/types'; import 'expo-app/lib/devClient'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { type Href, router, Stack } from 'expo-router'; import { useAtomValue } from 'jotai'; import { useEffect, useRef } from 'react'; @@ -204,7 +205,7 @@ export default function AppLayout() { - + , }} diff --git a/apps/expo/app/(app)/gear-inventory.tsx b/apps/expo/app/(app)/gear-inventory.tsx index 9a63d563ad..5f9e1bfafb 100644 --- a/apps/expo/app/(app)/gear-inventory.tsx +++ b/apps/expo/app/(app)/gear-inventory.tsx @@ -1,5 +1,6 @@ import { assertDefined } from '@packrat/guards'; import { Text } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { PackItemCard } from 'expo-app/features/packs/components/PackItemCard'; import { useUserPackItems } from 'expo-app/features/packs/hooks/useUserPackItems'; import type { PackItem } from 'expo-app/features/packs/types'; @@ -65,7 +66,7 @@ export default function GearInventoryScreen() { return ( - + diff --git a/apps/expo/app/(app)/messages/conversations.tsx b/apps/expo/app/(app)/messages/conversations.tsx index 99bfd519f8..3ba8e60484 100644 --- a/apps/expo/app/(app)/messages/conversations.tsx +++ b/apps/expo/app/(app)/messages/conversations.tsx @@ -14,6 +14,7 @@ import { Text, Toolbar, } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { Icon } from 'expo-app/components/Icon'; import { cn } from 'expo-app/lib/cn'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; @@ -80,8 +81,8 @@ export default function ConversationsIosScreen() { <> ( ), diff --git a/apps/expo/app/(app)/pack-categories/[id].tsx b/apps/expo/app/(app)/pack-categories/[id].tsx index 8ee9f33127..eef4ee3aef 100644 --- a/apps/expo/app/(app)/pack-categories/[id].tsx +++ b/apps/expo/app/(app)/pack-categories/[id].tsx @@ -1,4 +1,5 @@ import { Text } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { Icon, type MaterialIconName } from 'expo-app/components/Icon'; import { userStore } from 'expo-app/features/auth/store'; import { usePackDetailsFromStore } from 'expo-app/features/packs/hooks/usePackDetailsFromStore'; @@ -63,7 +64,7 @@ export default function PackCategoriesScreen() { return ( <> - + {categories.length ? ( diff --git a/apps/expo/app/(app)/pack-stats/[id].tsx b/apps/expo/app/(app)/pack-stats/[id].tsx index 7449de8e5e..35cf84af06 100644 --- a/apps/expo/app/(app)/pack-stats/[id].tsx +++ b/apps/expo/app/(app)/pack-stats/[id].tsx @@ -1,4 +1,5 @@ import { Button, Text } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { featureFlags } from 'expo-app/config'; import { userStore } from 'expo-app/features/auth/store'; import { usePackDetailsFromStore } from 'expo-app/features/packs/hooks/usePackDetailsFromStore'; @@ -34,7 +35,7 @@ export default function PackStatsScreen() { return ( {/* Weight History Section */} diff --git a/apps/expo/app/(app)/recent-packs.tsx b/apps/expo/app/(app)/recent-packs.tsx index b2377ed2c6..b07537dd7e 100644 --- a/apps/expo/app/(app)/recent-packs.tsx +++ b/apps/expo/app/(app)/recent-packs.tsx @@ -1,4 +1,5 @@ import { Text } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { Icon } from 'expo-app/components/Icon'; import type { Pack } from 'expo-app/features/packs'; import { useRecentPacks } from 'expo-app/features/packs/hooks/useRecentPacks'; @@ -66,7 +67,7 @@ export default function RecentPacksScreen() { return ( - + {recentPacks.length ? ( diff --git a/apps/expo/app/(app)/season-suggestions-results.tsx b/apps/expo/app/(app)/season-suggestions-results.tsx index 86906c1376..ed3c123d01 100644 --- a/apps/expo/app/(app)/season-suggestions-results.tsx +++ b/apps/expo/app/(app)/season-suggestions-results.tsx @@ -1,4 +1,5 @@ import { Text } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import * as Burnt from 'burnt'; import { Icon } from 'expo-app/components/Icon'; import { PackItemImage } from 'expo-app/features/packs/components/PackItemImage'; @@ -377,7 +378,7 @@ export default function SeasonSuggestionsResultsScreen() { return ( <> - + diff --git a/apps/expo/app/(app)/season-suggestions.tsx b/apps/expo/app/(app)/season-suggestions.tsx index 9d7cb0c729..b2cc57ce44 100644 --- a/apps/expo/app/(app)/season-suggestions.tsx +++ b/apps/expo/app/(app)/season-suggestions.tsx @@ -1,6 +1,7 @@ import type { BottomSheetModal } from '@gorhom/bottom-sheet'; import { assertDefined } from '@packrat/guards'; import { Button, Text } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import * as Sentry from '@sentry/react-native'; import { Icon } from 'expo-app/components/Icon'; import { LocationSearchSheet } from 'expo-app/features/packs/components/LocationSearchSheet'; @@ -112,7 +113,7 @@ export default function SeasonSuggestionsScreen() { return ( <> - + diff --git a/apps/expo/app/(app)/shared-packs.tsx b/apps/expo/app/(app)/shared-packs.tsx index 2c90dd1dac..f90e653b1d 100644 --- a/apps/expo/app/(app)/shared-packs.tsx +++ b/apps/expo/app/(app)/shared-packs.tsx @@ -1,4 +1,5 @@ import { Avatar, AvatarFallback, AvatarImage, Text } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { cn } from 'expo-app/lib/cn'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { Stack } from 'expo-router'; @@ -176,7 +177,7 @@ export default function SharedPacksScreen() { const { t } = useTranslation(); return ( - + diff --git a/apps/expo/app/(app)/shopping-list.tsx b/apps/expo/app/(app)/shopping-list.tsx index d5b7d85cc5..8b2fb77988 100644 --- a/apps/expo/app/(app)/shopping-list.tsx +++ b/apps/expo/app/(app)/shopping-list.tsx @@ -1,6 +1,7 @@ 'use client'; import { Text } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { Icon } from 'expo-app/components/Icon'; import { cn } from 'expo-app/lib/cn'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; @@ -171,7 +172,7 @@ export default function ShoppingListScreen() { return ( - + diff --git a/apps/expo/app/(app)/trail-conditions.tsx b/apps/expo/app/(app)/trail-conditions.tsx index c9025d9d1c..441185aca7 100644 --- a/apps/expo/app/(app)/trail-conditions.tsx +++ b/apps/expo/app/(app)/trail-conditions.tsx @@ -1,4 +1,5 @@ import { ActivityIndicator, Text } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { featureFlags } from 'expo-app/config'; import { SubmitConditionReportForm } from 'expo-app/features/trail-conditions/components/SubmitConditionReportForm'; import { TrailConditionReportCard } from 'expo-app/features/trail-conditions/components/TrailConditionReportCard'; @@ -163,8 +164,8 @@ export default function TrailConditionsScreen() { ( setShowSubmitForm(true)} diff --git a/apps/expo/app/(app)/weather-alert-preferences.tsx b/apps/expo/app/(app)/weather-alert-preferences.tsx index 4ca305da42..fd06fcaeb0 100644 --- a/apps/expo/app/(app)/weather-alert-preferences.tsx +++ b/apps/expo/app/(app)/weather-alert-preferences.tsx @@ -1,4 +1,5 @@ import { Form, FormItem, FormSection, Text, Toggle } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { Icon } from 'expo-app/components/Icon'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; @@ -66,7 +67,7 @@ export default function WeatherAlertPreferencesScreen() { return ( <> - + - + - - + - + diff --git a/apps/expo/features/feed/screens/FeedScreen.tsx b/apps/expo/features/feed/screens/FeedScreen.tsx index ee00aa0a3a..78fbe688de 100644 --- a/apps/expo/features/feed/screens/FeedScreen.tsx +++ b/apps/expo/features/feed/screens/FeedScreen.tsx @@ -1,4 +1,5 @@ import { ActivityIndicator, Button, Text } from '@packrat/ui/nativewindui'; +import { getAppBarOptions } from '@packrat/ui/src/app-bar'; import { Icon } from 'expo-app/components/Icon'; import { userStore } from 'expo-app/features/auth/store'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; @@ -87,8 +88,8 @@ export const FeedScreen = () => { (