refactor(mobile): centralize safe-area spacing in useScreenInsets#2314
Merged
Conversation
Screens previously hand-rolled their bottom/top safe-area spacing, each calling useSafeAreaInsets() and adding its own magic offset (12/16/20/24/32/40/50). The device insets are the right cross-platform primitive, but the policy on top of them was duplicated and inconsistent. Add a useScreenInsets hook that owns that policy with a rationalized 12/24/40 scale (compact/default/roomy) plus composerBottom, fabBottom, and contentTop helpers. Extract the duplicated bottom-sheet shell into a SheetContainer component (backdrop, panel, shadow, drag handle, inset) and move SelectSheet/AttachmentSheet onto it. Migrate FABs, composers, page sheets, and the mcp/settings/pr-diff/ automation-create screens onto the hook. Two values snap to the scale (settings 32->24, pr-diff 16->24); everything else maps exactly. Functional clearances (clearing a sticky footer or floating button, measured header heights) are left bespoke since they aren't safe-area policy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/mobile/src/app/task/[id].tsx:62-63
`composerBottom` is a plain JS function from `useMemo`, not a Reanimated worklet. `useAnimatedStyle` callbacks that read a `SharedValue` (`height.value`) are serialized and executed on the UI thread, where calling non-worklet JS functions throws a runtime error. The sibling file `task/index.tsx` avoids this correctly by pre-computing `restingBottom = bottom("compact")` outside the animated style and capturing the primitive number in the worklet closure. The same pattern should be applied here.
```suggestion
const { insets, composerBottom } = useScreenInsets();
const composerBottomValue = composerBottom();
const themeColors = useThemeColors();
```
### Issue 2 of 2
apps/mobile/src/app/task/[id].tsx:125-128
Once the value is pre-computed outside the worklet, use the scalar directly and update the dependency array accordingly.
```suggestion
return {
marginBottom: height.value < 0 ? 0 : composerBottomValue,
};
}, [composerBottomValue]);
```
Reviews (1): Last reviewed commit: "refactor(mobile): centralize safe-area s..." | Re-trigger Greptile |
dmarticus
approved these changes
May 23, 2026
Contributor
dmarticus
left a comment
There was a problem hiding this comment.
Nice cleanup. The hook and SheetContainer both pull their weight — 12/24/40 is the right level of abstraction for the scatter it's replacing, and the sheet shell is genuinely shared rather than over-fit. Approving; comments below are non-blocking nits to look at before/after merge.
One thing worth eyeballing on a simulator given you flagged it: the two declared 8px shifts (settings 32→24, pr-diff 16→24) and the roomy variant collapsing the previous +40 sites — 60 seconds on iPhone SE + Pro Max in both modes should be enough.
- Pre-compute composerBottom() outside the useAnimatedStyle worklet in task/[id].tsx so the UI thread never calls the non-worklet getter - Rename contentTop() -> sheetContentTop() to match its sheet-only scope - Document fabBottom()/composerBottom() as frozen, non-variant choices - Note compact is the inherited sheet default on SheetContainer - Accept a style prop on SheetContainer for measured overrides - Add a unit test pinning the spacing scale
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
On mobile, screens each called
useSafeAreaInsets()and then hand-added their own magic offset for top/bottom spacing (12 / 16 / 20 / 24 / 32 / 40 / 50). The device insets are the correct cross-platform/cross-device primitive — and they're used — but the policy layered on top was duplicated and inconsistent per screen. (Originated from a question about the gap under the task-screen composer:Math.max(insets.bottom, 50), correct for that screen but not shared anywhere.)What
useScreenInsetshook — single source of truth for spacing policy. WrapsuseSafeAreaInsets()and exposes a rationalized 12/24/40 scale viabottom("compact" | "default" | "roomy"), pluscomposerBottom(),fabBottom(),contentTop(), and rawinsets.SheetContainercomponent — extracts the duplicated bottom-sheet shell (backdrop, pinned panel, rounded top, border, shadow, drag handle, inset-aware padding).SelectSheetandAttachmentSheetnow render through it.Rationalization (intentional)
Two values snap onto the scale; everything else maps exactly:
settingsbottom: 32 → 24pr-diffbottom: 16 → 24Left bespoke on purpose
Functional clearances that aren't safe-area policy: DismissReportSheet's
+120(clears its sticky footer), automation/create's computed floating-button clearance, inbox/[id]'s+100/+16, and measured floating-header heights (insets.top + 60/64).Testing
biome check: cleantsc --noEmit: no new errors (pre-existing test-file errors unchanged vs.main)🤖 Generated with Claude Code