-
Notifications
You must be signed in to change notification settings - Fork 321
Mobile #1113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Ashraf-Ali-aa
wants to merge
12
commits into
RunMaestro:main
Choose a base branch
from
Ashraf-Ali-aa:mobile
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Mobile #1113
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
6b765e8
feat(mobile-pairing): add QR-based device pairing module
Ashraf-Ali-aa 763dc50
feat(mobile-pairing): add IPC handlers and preload bridge
Ashraf-Ali-aa 90ceec4
feat(web-server): support mobile device token authentication
Ashraf-Ali-aa cabd655
feat(settings): add mobile devices pairing UI
Ashraf-Ali-aa 7074112
refactor(offline-queue): abstract storage for cross-platform support
Ashraf-Ali-aa a8f7937
test(web-server): add mobile token authentication test coverage
Ashraf-Ali-aa cce1697
chore(ci): add mobile app pipeline with path-based triggering
Ashraf-Ali-aa d2e2a27
feat(mobile): scaffold Expo React Native companion app
Ashraf-Ali-aa bdddf45
chore(mobile): exclude Expo generated/native artifacts from prettier
Ashraf-Ali-aa b71347d
fix CR
Ashraf-Ali-aa b5f103a
fix(mobile): address CodeRabbit review feedback on mobile PR
Ashraf-Ali-aa 60120f0
ci(mobile): pin GitHub Actions to immutable commit SHAs
Ashraf-Ali-aa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,298 @@ | ||
| --- | ||
| name: building-native-ui | ||
| description: Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs. | ||
| version: 1.0.1 | ||
| license: MIT | ||
| --- | ||
|
|
||
| # Expo UI Guidelines | ||
|
|
||
| ## References | ||
|
|
||
| Consult these resources as needed: | ||
|
|
||
| ```text | ||
| references/ | ||
| animations.md Reanimated: entering, exiting, layout, scroll-driven, gestures | ||
| controls.md Native iOS: Switch, Slider, SegmentedControl, DateTimePicker, Picker | ||
| form-sheet.md Form sheets in expo-router: configuration, footers and background interaction. | ||
| gradients.md CSS gradients via experimental_backgroundImage (New Arch only) | ||
| icons.md SF Symbols via expo-image (sf: source), names, animations, weights | ||
| media.md Camera, audio, video, and file saving | ||
| route-structure.md Route conventions, dynamic routes, groups, folder organization | ||
| search.md Search bar with headers, useSearch hook, filtering patterns | ||
| storage.md SQLite, AsyncStorage, SecureStore | ||
| tabs.md NativeTabs, migration from JS tabs, iOS 26 features | ||
| toolbar-and-headers.md Stack headers and toolbar buttons, menus, search (iOS only) | ||
| visual-effects.md Blur (expo-blur) and liquid glass (expo-glass-effect) | ||
| webgpu-three.md 3D graphics, games, GPU visualizations with WebGPU and Three.js | ||
| zoom-transitions.md Apple Zoom: fluid zoom transitions with Link.AppleZoom (iOS 18+) | ||
| ``` | ||
|
|
||
| ## Running the App | ||
|
|
||
| **CRITICAL: This app requires a custom Expo development build and will not work in Expo Go.** | ||
|
|
||
| Do not attempt to run this app with the Expo Go client - it relies on native modules and configuration that Expo Go does not ship with. Use a custom dev build instead: | ||
|
|
||
| 1. **iOS**: `npx expo run:ios` (or `npx serve-sim` for the simulator verification flow used in this repo) | ||
| 2. **Android**: `npx expo run:android` | ||
| 3. **Web**: `npx agent-browser` | ||
|
|
||
| ### Why a Custom Build Is Required | ||
|
|
||
| This project pulls in capabilities Expo Go does not support, including: | ||
|
|
||
| - **Local Expo modules** (custom native code in `modules/`) | ||
| - **Apple targets** (widgets, app clips, extensions via `@bacons/apple-targets`) | ||
| - **Third-party native modules** not bundled with Expo Go | ||
| - **Custom native configuration** that can't be expressed in `app.json` alone | ||
|
|
||
| If a build fails, fix the native config or run `npx expo prebuild` - do not fall back to Expo Go. | ||
|
|
||
| ## Code Style | ||
|
|
||
| - Be cautious of unterminated strings. Ensure nested backticks are escaped; never forget to escape quotes correctly. | ||
| - Always use import statements at the top of the file. | ||
| - Always use kebab-case for file names, e.g. `comment-card.tsx` | ||
| - Always remove old route files when moving or restructuring navigation | ||
| - Never use special characters in file names | ||
| - Configure tsconfig.json with path aliases, and prefer aliases over relative imports for refactors. | ||
|
|
||
| ## Routes | ||
|
|
||
| See `./references/route-structure.md` for detailed route conventions. | ||
|
|
||
| - Routes belong in the `app` directory. | ||
| - Never co-locate components, types, or utilities in the app directory. This is an anti-pattern. | ||
| - Ensure the app always has a route that matches "/", it may be inside a group route. | ||
|
|
||
| ## Library Preferences | ||
|
|
||
| - Never use modules removed from React Native such as Picker, WebView, SafeAreaView, or AsyncStorage | ||
| - Never use legacy expo-permissions | ||
| - `expo-audio` not `expo-av` | ||
| - `expo-video` not `expo-av` | ||
| - `expo-image` with `source="sf:name"` for SF Symbols, not `expo-symbols` or `@expo/vector-icons` | ||
| - `react-native-safe-area-context` not react-native SafeAreaView | ||
| - `process.env.EXPO_OS` not `Platform.OS` | ||
| - `React.use` not `React.useContext` | ||
| - `expo-image` Image component instead of intrinsic element `img` | ||
| - `expo-glass-effect` for liquid glass backdrops | ||
|
|
||
| ## Responsiveness | ||
|
|
||
| - Always wrap root component in a scroll view for responsiveness | ||
| - Use `<ScrollView contentInsetAdjustmentBehavior="automatic" />` instead of `<SafeAreaView>` for smarter safe area insets | ||
| - `contentInsetAdjustmentBehavior="automatic"` should be applied to FlatList and SectionList as well | ||
| - Use flexbox instead of Dimensions API | ||
| - ALWAYS prefer `useWindowDimensions` over `Dimensions.get()` to measure screen size | ||
|
|
||
| ## Behavior | ||
|
|
||
| - Use expo-haptics conditionally on iOS to make more delightful experiences | ||
| - Use views with built-in haptics like `<Switch />` from React Native and `@react-native-community/datetimepicker` | ||
| - When a route belongs to a Stack, its first child should almost always be a ScrollView with `contentInsetAdjustmentBehavior="automatic"` set | ||
| - When adding a `ScrollView` to the page it should almost always be the first component inside the route component | ||
| - Prefer `headerSearchBarOptions` in Stack.Screen options to add a search bar | ||
| - Use the `<Text selectable />` prop on text containing data that could be copied | ||
| - Consider formatting large numbers like 1.4M or 38k | ||
| - Never use intrinsic elements like 'img' or 'div' unless in a webview or Expo DOM component | ||
|
|
||
| # Styling | ||
|
|
||
| Follow Apple Human Interface Guidelines. | ||
|
|
||
| ## General Styling Rules | ||
|
|
||
| - Prefer flex gap over margin and padding styles | ||
| - Prefer padding over margin where possible | ||
| - Always account for safe area, either with stack headers, tabs, or ScrollView/FlatList `contentInsetAdjustmentBehavior="automatic"` | ||
| - Ensure both top and bottom safe area insets are accounted for | ||
| - Inline styles not StyleSheet.create unless reusing styles is faster | ||
| - Add entering and exiting animations for state changes | ||
| - Use `{ borderCurve: 'continuous' }` for rounded corners unless creating a capsule shape | ||
| - ALWAYS use a navigation stack title instead of a custom text element on the page | ||
| - When padding a ScrollView, use `contentContainerStyle` padding and gap instead of padding on the ScrollView itself (reduces clipping) | ||
| - CSS and Tailwind are not supported - use inline styles | ||
|
|
||
| ## Text Styling | ||
|
|
||
| - Add the `selectable` prop to every `<Text/>` element displaying important data or error messages | ||
| - Counters should use `{ fontVariant: 'tabular-nums' }` for alignment | ||
|
|
||
| ## Shadows | ||
|
|
||
| Use CSS `boxShadow` style prop. NEVER use legacy React Native shadow or elevation styles. | ||
|
|
||
| ```tsx | ||
| <View style={{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }} /> | ||
| ``` | ||
|
|
||
| 'inset' shadows are supported. | ||
|
|
||
| # Navigation | ||
|
|
||
| ## Link | ||
|
|
||
| Use `<Link href="/path" />` from 'expo-router' for navigation between routes. | ||
|
|
||
| ```tsx | ||
| import { Link } from 'expo-router'; | ||
|
|
||
| // Basic link | ||
| <Link href="/path" /> | ||
|
|
||
| // Wrapping custom components | ||
| <Link href="/path" asChild> | ||
| <Pressable>...</Pressable> | ||
| </Link> | ||
| ``` | ||
|
|
||
| Whenever possible, include a `<Link.Preview>` to follow iOS conventions. Add context menus and previews frequently to enhance navigation. | ||
|
|
||
| ## Stack | ||
|
|
||
| - ALWAYS use `_layout.tsx` files to define stacks | ||
| - Use Stack from 'expo-router/stack' for native navigation stacks | ||
|
|
||
| ### Page Title | ||
|
|
||
| Set the page title in Stack.Screen options: | ||
|
|
||
| ```tsx | ||
| <Stack.Screen options={{ title: 'Home' }} /> | ||
| ``` | ||
|
|
||
| ## Context Menus | ||
|
|
||
| Add long press context menus to Link components: | ||
|
|
||
| ```tsx | ||
| import { Link } from 'expo-router'; | ||
|
|
||
| <Link href="/settings" asChild> | ||
| <Link.Trigger> | ||
| <Pressable> | ||
| <Card /> | ||
| </Pressable> | ||
| </Link.Trigger> | ||
| <Link.Menu> | ||
| <Link.MenuAction title="Share" icon="square.and.arrow.up" onPress={handleSharePress} /> | ||
| <Link.MenuAction title="Block" icon="nosign" destructive onPress={handleBlockPress} /> | ||
| <Link.Menu title="More" icon="ellipsis"> | ||
| <Link.MenuAction title="Copy" icon="doc.on.doc" onPress={() => {}} /> | ||
| <Link.MenuAction title="Delete" icon="trash" destructive onPress={() => {}} /> | ||
| </Link.Menu> | ||
| </Link.Menu> | ||
| </Link>; | ||
| ``` | ||
|
|
||
| ## Link Previews | ||
|
|
||
| Use link previews frequently to enhance navigation: | ||
|
|
||
| ```tsx | ||
| <Link href="/settings"> | ||
| <Link.Trigger> | ||
| <Pressable> | ||
| <Card /> | ||
| </Pressable> | ||
| </Link.Trigger> | ||
| <Link.Preview /> | ||
| </Link> | ||
| ``` | ||
|
|
||
| Link preview can be used with context menus. | ||
|
|
||
| ## Modal | ||
|
|
||
| Present a screen as a modal: | ||
|
|
||
| ```tsx | ||
| <Stack.Screen name="modal" options={{ presentation: 'modal' }} /> | ||
| ``` | ||
|
|
||
| Prefer this to building a custom modal component. | ||
|
|
||
| ## Sheet | ||
|
|
||
| Present a screen as a dynamic form sheet: | ||
|
|
||
| ```tsx | ||
| <Stack.Screen | ||
| name="sheet" | ||
| options={{ | ||
| presentation: 'formSheet', | ||
| sheetGrabberVisible: true, | ||
| sheetAllowedDetents: [0.5, 1.0], | ||
| contentStyle: { backgroundColor: 'transparent' }, | ||
| }} | ||
| /> | ||
| ``` | ||
|
|
||
| - Using `contentStyle: { backgroundColor: "transparent" }` makes the background liquid glass on iOS 26+. | ||
|
|
||
| ## Common route structure | ||
|
|
||
| A standard app layout with tabs and stacks inside each tab: | ||
|
|
||
| ```text | ||
| app/ | ||
| _layout.tsx - <NativeTabs /> | ||
| (index,search)/ | ||
| _layout.tsx - <Stack /> | ||
| index.tsx - Main list | ||
| search.tsx - Search view | ||
| ``` | ||
|
|
||
| ```tsx | ||
| // app/_layout.tsx | ||
| import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs'; | ||
| import { Theme } from '../components/theme'; | ||
|
|
||
| export default function Layout() { | ||
| return ( | ||
| <Theme> | ||
| <NativeTabs> | ||
| <NativeTabs.Trigger name="(index)"> | ||
| <Icon sf="list.dash" /> | ||
| <Label>Items</Label> | ||
| </NativeTabs.Trigger> | ||
| <NativeTabs.Trigger name="(search)" role="search" /> | ||
| </NativeTabs> | ||
| </Theme> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| Create a shared group route so both tabs can push common screens: | ||
|
|
||
| ```tsx | ||
| // app/(index,search)/_layout.tsx | ||
| import { Stack } from 'expo-router/stack'; | ||
| import { PlatformColor } from 'react-native'; | ||
|
|
||
| export default function Layout({ segment }) { | ||
| const screen = segment.match(/\((.*)\)/)?.[1]!; | ||
| const titles: Record<string, string> = { index: 'Items', search: 'Search' }; | ||
|
|
||
| return ( | ||
| <Stack | ||
| screenOptions={{ | ||
| headerTransparent: true, | ||
| headerShadowVisible: false, | ||
| headerLargeTitleShadowVisible: false, | ||
| headerLargeStyle: { backgroundColor: 'transparent' }, | ||
| headerTitleStyle: { color: PlatformColor('label') }, | ||
| headerLargeTitle: true, | ||
| headerBlurEffect: 'none', | ||
| headerBackButtonDisplayMode: 'minimal', | ||
| }} | ||
| > | ||
| <Stack.Screen name={screen} options={{ title: titles[screen] }} /> | ||
| <Stack.Screen name="i/[id]" options={{ headerLargeTitle: false }} /> | ||
| </Stack> | ||
| ); | ||
| } | ||
| ``` |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.