Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
89c6880
feat: expo ui migration phase 1 + 2 — remove useColorScheme/cn adapte…
mikib0 Jun 14, 2026
7766c67
chore(migration): add tracker, docs, script, biome fixes, and config
mikib0 Jun 14, 2026
53e8790
chore: sort root package.json fields
mikib0 Jun 14, 2026
82bbc90
feat(migration): add SearchOverlay for animated search experience on …
mikib0 Jun 15, 2026
84dae33
fix(migration): move SearchOverlay to packages/ui, restore deleted co…
mikib0 Jun 15, 2026
11971d3
fix(migration): absorb SearchContentContainer into overlays, restore …
mikib0 Jun 15, 2026
4980d59
fix(migration): improve search overlay UX and empty states
mikib0 Jun 15, 2026
3c0cc70
fix(types): re-export missing nativewindui components and fix useColo…
mikib0 Jun 15, 2026
3d5921a
fix(migration): restore weather header and SearchInput border, remove…
mikib0 Jun 15, 2026
c8e703d
feat(app-bar): implement Android large title header across all screens
mikib0 Jun 15, 2026
b8dd81b
fix(search-overlay): style Android search pill to match reference
mikib0 Jun 15, 2026
207df5e
fix(search-overlay): use SearchInput pill directly — remove double-wr…
mikib0 Jun 15, 2026
585d2f7
fix(search-overlay): use plain TextInput — remove pill, match reference
mikib0 Jun 15, 2026
4078d47
fix(search-overlay): use card background for content area to match re…
mikib0 Jun 15, 2026
55bef8a
fix(search-overlay): larger back button (26dp, neutral grey), larger …
mikib0 Jun 15, 2026
8ebad2e
fix(search-overlay): use grey3 for neutral icon color, increase X ico…
mikib0 Jun 15, 2026
b0e2afb
fix(search-overlay): swap dark mode backgrounds, use grey2 icons in d…
mikib0 Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 65 additions & 75 deletions apps/expo/app/(app)/(tabs)/(home)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@

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 { 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';
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';
Expand All @@ -38,8 +34,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';
Expand Down Expand Up @@ -165,14 +160,12 @@ const DASHBOARD_GAP_PREFIX = appConfig.dashboard.gapPrefix;

export default function DashboardScreen() {
const [searchValue, setSearchValue] = useState('');
const searchBarRef = useRef<LargeTitleSearchBarMethods>(null);
const unlockSheetRef = useRef<BottomSheetModal>(null);
const { t } = useTranslation();
const router = useRouter();
const isFocused = useIsFocused();
const { hasMinimumItems } = useHasMinimumInventory(20);
const { announcementSeen } = useSeasonSuggestionsPrefs();

useEffect(() => {
if (!isFocused || !hasMinimumItems || announcementSeen) return;
const timer = setTimeout(() => {
Expand Down Expand Up @@ -249,71 +242,68 @@ export default function DashboardScreen() {

return (
<>
<LargeTitleHeader
title={t('dashboard.title')}
searchBar={{
ref: asNonNullableRef(searchBarRef),
onChangeText: setSearchValue,
placeholder: appConfig.dashboard.strings.searchPlaceholder,
content: (
<LargeTitleHeaderSearchContentContainer>
{searchValue ? (
<FlatList
data={filteredTiles}
keyExtractor={keyExtractor}
contentContainerClassName="gap-4 px-4 pb-4"
renderItem={({ item }) => {
assertIsString(item);
if (!item.startsWith(DASHBOARD_GAP_PREFIX) && arrayIncludes(TILE_NAMES, item)) {
const Component = tileInfo[item].component;
return (
<Pressable
key={item}
className="rounded-2xl overflow-hidden "
onPress={() => {
setSearchValue('');
searchBarRef.current?.clearText();
}}
>
<Component />
</Pressable>
);
}
return null;
}}
ListHeaderComponent={() =>
filteredTiles.length > 0 ? (
<Text className="px-4 py-2 text-sm text-muted-foreground">
{filteredTiles.length}{' '}
{filteredTiles.length === 1
? appConfig.dashboard.strings.resultSingular
: appConfig.dashboard.strings.resultPlural}
</Text>
) : null
}
ListEmptyComponent={() => (
<View className="items-center justify-center p-6">
<Icon name="file-search-outline" size={48} color="#9ca3af" />
<View className="h-4" />
<Text className="text-lg font-medium text-muted-foreground">
{t('dashboard.noResults')}
</Text>
<Text className="mt-1 text-center text-sm text-muted-foreground">
{t('dashboard.tryDifferent')}
</Text>
</View>
)}
/>
) : (
<View className="flex-1 items-center justify-center p-4">
<Text className="text-muted-foreground">{t('dashboard.searchPlaceholder')}</Text>
</View>
)}
</LargeTitleHeaderSearchContentContainer>
),
<Stack.Screen
options={{
...getAppBarOptions(),
title: t('dashboard.title'),
headerBackVisible: false,
}}
backVisible={false}
/>
<SearchOverlay
placeholder={appConfig.dashboard.strings.searchPlaceholder}
value={searchValue}
onChangeText={setSearchValue}
>
{searchValue ? (
<FlatList
data={filteredTiles}
keyExtractor={keyExtractor}
contentContainerClassName="gap-4 px-4 pb-4"
renderItem={({ item }) => {
assertIsString(item);
if (!item.startsWith(DASHBOARD_GAP_PREFIX) && arrayIncludes(TILE_NAMES, item)) {
const Component = tileInfo[item].component;
return (
<Pressable
key={item}
className="overflow-hidden rounded-2xl"
onPress={() => setSearchValue('')}
>
<Component />
</Pressable>
);
}
return null;
}}
ListHeaderComponent={() =>
filteredTiles.length > 0 ? (
<Text className="px-4 py-2 text-sm text-muted-foreground">
{filteredTiles.length}{' '}
{filteredTiles.length === 1
? appConfig.dashboard.strings.resultSingular
: appConfig.dashboard.strings.resultPlural}
</Text>
) : null
}
ListEmptyComponent={() => (
<View className="items-center justify-center p-6">
<Icon name="file-search-outline" size={48} color="#9ca3af" />
<View className="h-4" />
<Text className="text-lg font-medium text-muted-foreground">
{t('dashboard.noResults')}
</Text>
<Text className="mt-1 text-center text-sm text-muted-foreground">
{t('dashboard.tryDifferent')}
</Text>
</View>
)}
/>
) : (
<View className="flex-1 items-center justify-center p-4">
<Text className="text-muted-foreground">{t('dashboard.searchPlaceholder')}</Text>
</View>
)}
</SearchOverlay>

<List
contentContainerClassName="pt-4"
Expand Down
24 changes: 10 additions & 14 deletions apps/expo/app/(app)/(tabs)/profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import {
AvatarFallback,
AvatarImage,
Button,
LargeTitleHeader,
List,
ListItem,
type ListRenderItemInfo,
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';
Expand Down Expand Up @@ -81,8 +81,16 @@ function Profile() {
const { t } = useTranslation();

const SCREEN_OPTIONS = {
...getAppBarOptions(),
title: t('profile.profile'),
headerShown: false,
headerBackVisible: false,
headerRight: () => (
<View className="flex-row items-center gap-2 pr-2 pl-2">
<DemoIcon />

<SettingsIcon />
</View>
),
} as const;

// Generate display data based on user information
Expand Down Expand Up @@ -114,18 +122,6 @@ function Profile() {
<>
<Stack.Screen options={SCREEN_OPTIONS} />

<LargeTitleHeader
title={t('profile.profile')}
backVisible={false}
rightView={() => (
<View className="flex-row items-center gap-2 pr-2 pl-2">
<DemoIcon />

<SettingsIcon />
</View>
)}
/>

<List
contentContainerClassName="pt-8"
variant="insets"
Expand Down
14 changes: 3 additions & 11 deletions apps/expo/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -122,7 +123,6 @@ export default function AppLayout() {
/>
<Stack.Screen name="ai-chat" />
<Stack.Screen name="catalog/[id]" options={getCatalogItemDetailOptions(t)} />
<Stack.Screen name="weather/index" options={{ headerShown: false }} />
<Stack.Screen
name="weather/search"
options={{
Expand Down Expand Up @@ -161,7 +161,6 @@ export default function AppLayout() {
<Stack.Screen
name="recent-packs"
options={{
headerShown: false,
presentation: 'modal',
animation: 'slide_from_bottom',
}}
Expand All @@ -176,15 +175,13 @@ export default function AppLayout() {
<Stack.Screen
name="weight-analysis/[id]"
options={{
headerShown: false,
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
<Stack.Screen
name="pack-categories/[id]"
options={{
headerShown: false,
presentation: 'modal',
animation: 'slide_from_bottom',
}}
Expand All @@ -201,56 +198,51 @@ export default function AppLayout() {
<Stack.Screen
name="weather-alerts"
options={{
headerShown: false,
presentation: 'card',
animation: 'default',
}}
/>
<Stack.Screen
name="weather-alert-preferences"
options={{
headerLargeTitle: true,
...getAppBarOptions(),
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
<Stack.Screen
name="trail-conditions"
options={{
headerShown: false,
presentation: 'card',
animation: 'slide_from_bottom',
}}
/>
<Stack.Screen
name="gear-inventory"
options={{
headerShown: false,
presentation: 'card',
animation: 'slide_from_bottom',
}}
/>
<Stack.Screen
name="shopping-list"
options={{
headerShown: false,
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
<Stack.Screen
name="shared-packs"
options={{
headerShown: false,
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
<Stack.Screen
name="guides/index"
options={{
...getAppBarOptions(),
title: 'Guides',
headerLargeTitle: true,
}}
/>
<Stack.Screen
Expand Down
13 changes: 4 additions & 9 deletions apps/expo/app/(app)/current-pack/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import {
Avatar,
AvatarFallback,
AvatarImage,
LargeTitleHeader,
Text,
} from '@packrat/ui/nativewindui';
import { Avatar, AvatarFallback, AvatarImage, Text } from '@packrat/ui/nativewindui';
import { getAppBarOptions } from '@packrat/ui/src/app-bar';
import { userStore } from 'expo-app/features/auth/store';
import { usePackDetailsFromStore } from 'expo-app/features/packs/hooks/usePackDetailsFromStore';
import type { PackItem } from 'expo-app/features/packs/types';
Expand All @@ -13,7 +8,7 @@ import { cn } from 'expo-app/lib/cn';
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 { useLocalSearchParams } from 'expo-router';
import { Stack, useLocalSearchParams } from 'expo-router';
import type React from 'react';
import { ScrollView, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
Expand Down Expand Up @@ -135,7 +130,7 @@ export default function CurrentPackScreen() {

return (
<SafeAreaView className="flex-1" edges={['bottom']}>
<LargeTitleHeader title={t('packs.currentPack')} />
<Stack.Screen options={{ ...getAppBarOptions(), title: t('packs.currentPack') }} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Migrated header is likely still suppressed on iOS due parent route options.

Line 133 adds getAppBarOptions(), but apps/expo/app/(app)/_layout.tsx still sets headerShown: false for current-pack/[id]. Since iOS options from getAppBarOptions() do not set headerShown: true, this route can keep the header hidden on iOS.

Suggested fix
-        <Stack.Screen
-          name="current-pack/[id]"
-          options={{
-            headerShown: false,
-            presentation: 'modal',
-            animation: 'slide_from_bottom',
-          }}
-        />
+        <Stack.Screen
+          name="current-pack/[id]"
+          options={{
+            presentation: 'modal',
+            animation: 'slide_from_bottom',
+          }}
+        />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/app/`(app)/current-pack/[id].tsx at line 133, The Stack.Screen
options at line 133 in the current-pack/[id] route are spreading
getAppBarOptions() but the parent layout still has headerShown: false set for
this route. Since getAppBarOptions() does not explicitly set headerShown: true,
the parent's suppression persists on iOS. Add headerShown: true to the options
object (either within the spread or after it) to explicitly override the parent
route's header suppression and ensure the migrated header is visible on iOS.

<ScrollView
className="flex-1"
contentContainerStyle={{ paddingBottom: 32 }}
Expand Down
17 changes: 10 additions & 7 deletions apps/expo/app/(app)/demo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Button, LargeTitleHeader, Text } from '@packrat/ui/nativewindui';
import { Button, Text } from '@packrat/ui/nativewindui';
import { getAppBarOptions } from '@packrat/ui/src/app-bar';
import { FlashList } from '@shopify/flash-list';
import { Card } from 'expo-app/components/Card';
import { Icon } from 'expo-app/components/Icon';
import { ThemeToggle } from 'expo-app/components/ThemeToggle';
import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme';
import { useHeaderSearchBar } from 'expo-app/lib/hooks/useHeaderSearchBar';
import { useRouter } from 'expo-router';
import { Stack, useRouter } from 'expo-router';
import { useHeaderHeight } from 'expo-router/react-navigation';
import { cssInterop } from 'nativewind';
import type * as React from 'react';
Expand Down Expand Up @@ -36,11 +37,13 @@ export default function Screen() {

return (
<>
<LargeTitleHeader
title="Demo"
backVisible={false}
searchBar={{ iosHideWhenScrolling: true }}
rightView={() => <ThemeToggle />}
<Stack.Screen
options={{
...getAppBarOptions(),
title: 'Demo',
headerBackVisible: false,
headerRight: () => <ThemeToggle />,
}}
/>
<FlashList
contentInsetAdjustmentBehavior="automatic"
Expand Down
Loading
Loading