refactor(mobile): centralize safe-area spacing in useScreenInsets#2314
refactor(mobile): centralize safe-area spacing in useScreenInsets#2314Gilbert09 wants to merge 1 commit into
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>
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 |
| const { insets, composerBottom } = useScreenInsets(); | ||
| const themeColors = useThemeColors(); |
There was a problem hiding this comment.
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.
| const { insets, composerBottom } = useScreenInsets(); | |
| const themeColors = useThemeColors(); | |
| const { insets, composerBottom } = useScreenInsets(); | |
| const composerBottomValue = composerBottom(); | |
| const themeColors = useThemeColors(); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/mobile/src/app/task/[id].tsx
Line: 62-63
Comment:
`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();
```
How can I resolve this? If you propose a fix, please make it concise.| return { | ||
| marginBottom: height.value < 0 ? 0 : Math.max(insets.bottom, 50), | ||
| marginBottom: height.value < 0 ? 0 : composerBottom(), | ||
| }; | ||
| }, [insets.bottom]); | ||
| }, [composerBottom]); |
There was a problem hiding this comment.
Once the value is pre-computed outside the worklet, use the scalar directly and update the dependency array accordingly.
| return { | |
| marginBottom: height.value < 0 ? 0 : Math.max(insets.bottom, 50), | |
| marginBottom: height.value < 0 ? 0 : composerBottom(), | |
| }; | |
| }, [insets.bottom]); | |
| }, [composerBottom]); | |
| return { | |
| marginBottom: height.value < 0 ? 0 : composerBottomValue, | |
| }; | |
| }, [composerBottomValue]); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/mobile/src/app/task/[id].tsx
Line: 125-128
Comment:
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]);
```
How can I resolve this? If you propose a fix, please make it concise.
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