From 4b88412e775cd62bb8a78cc8cf38738882d65fd2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 16:53:50 +0000 Subject: [PATCH 1/6] fix(ExampleApp): respect safe area insets on HomeScreen The HomeScreen rendered a bare FlatList instead of using the Screen component, and the navigator hides the native header, so the title and list content slid under the status bar/notch. Apply top/left/right safe area insets to the list so content stays clear of the safe areas. --- apps/ExampleApp/app/screens/HomeScreen/HomeScreen.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/ExampleApp/app/screens/HomeScreen/HomeScreen.tsx b/apps/ExampleApp/app/screens/HomeScreen/HomeScreen.tsx index 683dd4f9..6be8d2e7 100644 --- a/apps/ExampleApp/app/screens/HomeScreen/HomeScreen.tsx +++ b/apps/ExampleApp/app/screens/HomeScreen/HomeScreen.tsx @@ -8,6 +8,7 @@ import { DEMO_LIST, DemoInfo } from "./demoInfo" import { DemoListItem } from "./components/DemoListItem" import { useTypedNavigation } from "../../navigators/useTypedNavigation" import { colors } from "../../theme" +import { useSafeAreaInsetsStyle } from "../../utils/useSafeAreaInsetsStyle" type HomeScreenProps = NativeStackScreenProps> @@ -18,6 +19,10 @@ export const HomeScreen: FC = observer(function HomeScreen() { // Pull in navigation via hook const navigation = useTypedNavigation<"Home">() + // The navigator hides the native header, so apply the safe area insets here + // to keep content clear of the status bar, notch, and home indicator. + const $safeAreaInsets = useSafeAreaInsetsStyle(["top", "left", "right"]) + const renderItem = React.useCallback( ({ item }: { item: DemoInfo }) => { const onPress = () => navigation.navigate(item.screen) @@ -38,6 +43,7 @@ export const HomeScreen: FC = observer(function HomeScreen() { } data={DEMO_LIST} renderItem={renderItem} + style={[$listStyle, $safeAreaInsets]} contentContainerStyle={$contentContainerStyle} /> ) @@ -49,6 +55,8 @@ const $shadowSpace: ViewStyle = { zIndex: 1, } +const $listStyle: ViewStyle = { backgroundColor: colors.background } + const $contentContainerStyle: ViewStyle = { paddingBottom: 100, paddingTop: 24 } const $titleContainer: ViewStyle = { From d6c39b4fa9afac266a799636d2664f696008496a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 17:10:12 +0000 Subject: [PATCH 2/6] refactor(ExampleApp): use Screen component for HomeScreen Replace the manual safe-area inset hack with the shared Screen component used by every other screen. Uses the "fixed" preset so the FlatList keeps its own scrolling (a VirtualizedList must not be nested in a ScrollView), with the inner container flexed so the list fills the safe area. --- .../app/screens/HomeScreen/HomeScreen.tsx | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/apps/ExampleApp/app/screens/HomeScreen/HomeScreen.tsx b/apps/ExampleApp/app/screens/HomeScreen/HomeScreen.tsx index 6be8d2e7..27675876 100644 --- a/apps/ExampleApp/app/screens/HomeScreen/HomeScreen.tsx +++ b/apps/ExampleApp/app/screens/HomeScreen/HomeScreen.tsx @@ -3,12 +3,11 @@ import { observer } from "mobx-react-lite" import { ViewStyle, FlatList, View } from "react-native" import { NativeStackScreenProps } from "@react-navigation/native-stack" import { AppStackScreenProps } from "app/navigators" -import { Text } from "app/components" +import { Screen, Text } from "app/components" import { DEMO_LIST, DemoInfo } from "./demoInfo" import { DemoListItem } from "./components/DemoListItem" import { useTypedNavigation } from "../../navigators/useTypedNavigation" import { colors } from "../../theme" -import { useSafeAreaInsetsStyle } from "../../utils/useSafeAreaInsetsStyle" type HomeScreenProps = NativeStackScreenProps> @@ -19,10 +18,6 @@ export const HomeScreen: FC = observer(function HomeScreen() { // Pull in navigation via hook const navigation = useTypedNavigation<"Home">() - // The navigator hides the native header, so apply the safe area insets here - // to keep content clear of the status bar, notch, and home indicator. - const $safeAreaInsets = useSafeAreaInsetsStyle(["top", "left", "right"]) - const renderItem = React.useCallback( ({ item }: { item: DemoInfo }) => { const onPress = () => navigation.navigate(item.screen) @@ -32,31 +27,43 @@ export const HomeScreen: FC = observer(function HomeScreen() { [navigation], ) + // Use a "fixed" preset so the FlatList does its own scrolling — a + // VirtualizedList must not be nested inside the ScrollView that the + // "scroll" preset would provide. return ( - - - + + + + + - - } - data={DEMO_LIST} - renderItem={renderItem} - style={[$listStyle, $safeAreaInsets]} - contentContainerStyle={$contentContainerStyle} - /> + } + data={DEMO_LIST} + renderItem={renderItem} + style={$list} + contentContainerStyle={$contentContainerStyle} + /> + ) }) +// Let the inner container fill the screen so the FlatList can size itself. +const $screenContentContainer: ViewStyle = { flex: 1 } + +const $list: ViewStyle = { flex: 1 } + const $shadowSpace: ViewStyle = { paddingBottom: 4, backgroundColor: "rgba(0,0,0,0)", zIndex: 1, } -const $listStyle: ViewStyle = { backgroundColor: colors.background } - const $contentContainerStyle: ViewStyle = { paddingBottom: 100, paddingTop: 24 } const $titleContainer: ViewStyle = { From 133b098f64549d3c377e28822c37fd585aae8f88 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 17:44:32 +0000 Subject: [PATCH 3/6] feat(ExampleApp): present image picker as a page sheet on iOS The picker defaulted to the system's full-screen presentation, whose nav bar renders under the status bar / Dynamic Island. Set presentationStyle to PAGE_SHEET on the shared picker options so it presents as a card sheet that sits below the safe area. Applies to both the library and camera flows, which share these options. --- apps/ExampleApp/app/utils/useExampleImage/useExampleImage.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/ExampleApp/app/utils/useExampleImage/useExampleImage.ts b/apps/ExampleApp/app/utils/useExampleImage/useExampleImage.ts index 10e2c79e..1563e3f3 100644 --- a/apps/ExampleApp/app/utils/useExampleImage/useExampleImage.ts +++ b/apps/ExampleApp/app/utils/useExampleImage/useExampleImage.ts @@ -5,6 +5,7 @@ import { ImagePickerOptions, ImagePickerAsset, useCameraPermissions, + UIImagePickerPresentationStyle, } from "expo-image-picker" import { useCallback, useState, useMemo } from "react" import { useAssets, Asset } from "expo-asset" @@ -59,6 +60,9 @@ const IMAGE_PICKER_OPTIONS: ImagePickerOptions = { mediaTypes: "images", allowsEditing: false, quality: 0.5, + // Present as a card sheet (iOS) instead of the default full-screen picker, + // which renders its nav bar under the status bar / Dynamic Island. + presentationStyle: UIImagePickerPresentationStyle.PAGE_SHEET, } export function useExampleImage(predicates?: { From e5e973d9dc443cf161e55d8f0a202ca6cde17b67 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 17:55:59 +0000 Subject: [PATCH 4/6] fix(ExampleApp): keep camera full-screen, only page-sheet the library picker Split the shared picker options so launchImageLibraryAsync uses a page sheet (avoiding the full-screen nav bar under the status bar) while launchCameraAsync keeps the conventional full-screen camera. --- .../utils/useExampleImage/useExampleImage.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/ExampleApp/app/utils/useExampleImage/useExampleImage.ts b/apps/ExampleApp/app/utils/useExampleImage/useExampleImage.ts index 1563e3f3..ba6c770a 100644 --- a/apps/ExampleApp/app/utils/useExampleImage/useExampleImage.ts +++ b/apps/ExampleApp/app/utils/useExampleImage/useExampleImage.ts @@ -60,11 +60,22 @@ const IMAGE_PICKER_OPTIONS: ImagePickerOptions = { mediaTypes: "images", allowsEditing: false, quality: 0.5, - // Present as a card sheet (iOS) instead of the default full-screen picker, - // which renders its nav bar under the status bar / Dynamic Island. +} + +// The photo library picker presents as a card sheet (iOS) instead of the +// default full-screen presentation, whose nav bar renders under the status +// bar / Dynamic Island. +const LIBRARY_PICKER_OPTIONS: ImagePickerOptions = { + ...IMAGE_PICKER_OPTIONS, presentationStyle: UIImagePickerPresentationStyle.PAGE_SHEET, } +// The camera stays full-screen, which is the conventional capture experience. +const CAMERA_PICKER_OPTIONS: ImagePickerOptions = { + ...IMAGE_PICKER_OPTIONS, + presentationStyle: UIImagePickerPresentationStyle.FULL_SCREEN, +} + export function useExampleImage(predicates?: { filter?: ImageFilter groupBy?: ImageGrouper @@ -114,7 +125,7 @@ export function useExampleImage(predicates?: { const selectPhoto = useCallback(async () => { setStatus("selectingPhoto") - const result: ImagePickerResult = await launchImageLibraryAsync(IMAGE_PICKER_OPTIONS) + const result: ImagePickerResult = await launchImageLibraryAsync(LIBRARY_PICKER_OPTIONS) if (result.assets?.[0]) { setImage({ ...result.assets?.[0], localUri: result.assets?.[0].uri } as SelectedImage) } else { @@ -131,7 +142,7 @@ export function useExampleImage(predicates?: { return } setStatus("takingPhoto") - const result: ImagePickerResult = await launchCameraAsync(IMAGE_PICKER_OPTIONS) + const result: ImagePickerResult = await launchCameraAsync(CAMERA_PICKER_OPTIONS) if (result.assets?.[0]) { setImage({ ...result.assets?.[0], localUri: result.assets?.[0].uri } as SelectedImage) } else { From 61d80a2f96e7c6a25336ca5d5eb04320785ef9ea Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 18:09:33 +0000 Subject: [PATCH 5/6] chore: add empty changeset Changes are confined to the private example-app workspace, so no package version bump is needed. The empty changeset satisfies the changeset check. --- .changeset/poor-lamps-share.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/poor-lamps-share.md diff --git a/.changeset/poor-lamps-share.md b/.changeset/poor-lamps-share.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/poor-lamps-share.md @@ -0,0 +1,2 @@ +--- +--- From f0830d39d6a43566b5a3a63600db2aab0f417429 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 18:10:10 +0000 Subject: [PATCH 6/6] docs: add CLAUDE.md with changeset guidance Document that PRs touching only non-published workspaces (e.g. example-app) should add an empty changeset (npx changeset --empty) to satisfy the changeset check without bumping any package version. --- CLAUDE.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..b0525afe --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,19 @@ +# Project instructions + +## Changesets + +This repo uses [changesets](https://github.com/changesets/changesets), and a +changeset-bot check runs on every PR. + +- If a PR changes a **published package** (anything under `modules/`), add a + changeset describing the change and its semver bump (`npx changeset`). +- If a PR's changes are confined to **non-published workspaces** (e.g. the + `example-app` in `apps/`, docs, CI config), no version bump is needed — but + the changeset check still requires a changeset to be present. In that case add + an **empty changeset** so the check passes without bumping any package: + + ```sh + npx changeset --empty + ``` + + Commit the generated `.changeset/*.md` file along with the PR.