From ac388f7158d7cb955a64ddda33662a22c7dd1f72 Mon Sep 17 00:00:00 2001 From: DaveyEke Date: Fri, 27 Feb 2026 18:37:19 +0100 Subject: [PATCH] feat(one): add NativeTabs as first-class export with cross-platform icon support --- .../data/docs/components-NativeTabs.mdx | 106 ++++++++++++++++++ .../data/docs/exports-withLayoutContext.mdx | 8 +- .../data/docs/routing-layouts.mdx | 1 + .../onestack.dev/features/docs/docsRoutes.tsx | 1 + bun.lock | 7 +- examples/testflight/assets/icons/bell.svg | 1 + .../testflight/assets/icons/newspaper.svg | 1 + examples/testflight/assets/icons/user.svg | 1 + .../code/home/HomeLayout.native.tsx | 15 ++- .../testflight/code/ui/BottomTabs.native.tsx | 5 +- packages/one/package.json | 19 ++++ packages/one/src/layouts/NativeTabs.tsx | 24 ++++ packages/one/src/native-tabs.ts | 1 + packages/one/types/layouts/NativeTabs.d.ts | 6 + packages/one/types/native-tabs.d.ts | 2 + 15 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 apps/onestack.dev/data/docs/components-NativeTabs.mdx create mode 100644 examples/testflight/assets/icons/bell.svg create mode 100644 examples/testflight/assets/icons/newspaper.svg create mode 100644 examples/testflight/assets/icons/user.svg create mode 100644 packages/one/src/layouts/NativeTabs.tsx create mode 100644 packages/one/src/native-tabs.ts create mode 100644 packages/one/types/layouts/NativeTabs.d.ts create mode 100644 packages/one/types/native-tabs.d.ts diff --git a/apps/onestack.dev/data/docs/components-NativeTabs.mdx b/apps/onestack.dev/data/docs/components-NativeTabs.mdx new file mode 100644 index 0000000000..b4e9ee46aa --- /dev/null +++ b/apps/onestack.dev/data/docs/components-NativeTabs.mdx @@ -0,0 +1,106 @@ +--- +title: "" +--- + +The NativeTabs component provides a native platform tab bar using [`react-native-bottom-tabs`](https://github.com/okwasniewski/react-native-bottom-tabs). Unlike the regular [Tabs](/docs/components-Tabs) component which uses a JavaScript-based tab bar, NativeTabs renders using the platform's native tab bar (`UITabBarController` on iOS, Material Bottom Tabs on Android), giving you native feel and SF Symbol support on iOS. + +Import it from `one/native-tabs` to keep the bundle size minimal when not using native tabs. + +This component should only be rendered inside a `_layout.tsx` file, where it will serve as the location that children will render for routes below the layout. + +NativeTabs wraps the navigator from [`@bottom-tabs/react-navigation`](https://github.com/react-navigation/react-navigation) and accepts the same props. + +## Basic Usage + +```tsx fileName="_layout.native.tsx" +import { NativeTabs } from 'one/native-tabs' + +export default function Layout() { + return ( + + ({ sfSymbol: 'house' }), + }} + /> + ({ sfSymbol: 'person' }), + }} + /> + + ) +} +``` + +## Tab Icons + +On iOS, you can use [SF Symbols](https://developer.apple.com/sf-symbols/) by returning an object with `sfSymbol`: + +```tsx +tabBarIcon: () => ({ sfSymbol: 'house' }) +``` + +On Android, SF Symbols are not available. You can use imported SVG or image assets instead: + +```tsx fileName="_layout.native.tsx" +import { Platform } from 'react-native' +import { NativeTabs } from 'one/native-tabs' +import homeIcon from '../assets/icons/home.svg' + +export default function Layout() { + return ( + + + Platform.OS === 'ios' ? { sfSymbol: 'house' } : homeIcon, + }} + /> + + ) +} +``` + + + Use `import` for assets rather than `require()`. + + +## Platform-Specific Layouts + +Since NativeTabs is native-only, you'll typically pair it with a web layout using [platform-specific file extensions](/docs/routing): + +``` +app/ +├── _layout.tsx # Web layout (Tabs or custom) +├── _layout.native.tsx # Native layout (NativeTabs) +├── index.tsx +└── profile.tsx +``` + +## Common Options + +| Option | Type | Description | +|--------|------|-------------| +| `title` | `string` | Tab label text | +| `tabBarIcon` | `() => ImageSource \| { sfSymbol: string }` | Tab icon (SF Symbol on iOS, image asset on Android) | +| `tabBarBadge` | `string` | Badge text on the tab | +| `tabBarActiveTintColor` | `string` | Active tab tint color | + +## Dependencies + +NativeTabs requires these optional peer dependencies to be installed: + +```bash +npm install @bottom-tabs/react-navigation react-native-bottom-tabs +``` + +These are marked as optional peer dependencies in `one`, so they won't be installed automatically. This keeps bundle sizes smaller for apps that don't use NativeTabs. + +For more configuration options, see the [`react-native-bottom-tabs` documentation](https://github.com/okwasniewski/react-native-bottom-tabs). diff --git a/apps/onestack.dev/data/docs/exports-withLayoutContext.mdx b/apps/onestack.dev/data/docs/exports-withLayoutContext.mdx index 7d15f436ba..aceecc80ab 100644 --- a/apps/onestack.dev/data/docs/exports-withLayoutContext.mdx +++ b/apps/onestack.dev/data/docs/exports-withLayoutContext.mdx @@ -3,11 +3,9 @@ title: withLayoutContext description: Create your own layouts. --- -Today there are a few built-in navigators: [Slot](/docs/components-Slot), [Stack](/docs/components-Stack), etc. But you may want to create your own. +Today there are a few built-in navigators: [Slot](/docs/components-Slot), [Stack](/docs/components-Stack), [Tabs](/docs/components-Tabs), [NativeTabs](/docs/components-NativeTabs), and [Drawer](/docs/components-Drawer). But you may want to create your own. -A good example of this is the new [`react-native-bottom-tabs`](https://github.com/okwasniewski/react-native-bottom-tabs) library, that exports a React Navigation compatible navigator. - -To make any React Navigation navigator work inside a One layout, use `withLayoutContext`: +To make any React Navigation navigator work inside a One layout, use `withLayoutContext`. For example, this is how [NativeTabs](/docs/components-NativeTabs) is built: ```tsx import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation' @@ -20,4 +18,4 @@ export const NativeTabs = withLayoutContext( ) ``` -Now you can use `NativeTabs` in any `_layout.tsx` file. +Now you can use `NativeTabs` in any `_layout.tsx` file. This same pattern works with any React Navigation compatible navigator. diff --git a/apps/onestack.dev/data/docs/routing-layouts.mdx b/apps/onestack.dev/data/docs/routing-layouts.mdx index 3f46a68447..ca9c820e7d 100644 --- a/apps/onestack.dev/data/docs/routing-layouts.mdx +++ b/apps/onestack.dev/data/docs/routing-layouts.mdx @@ -35,6 +35,7 @@ Layouts must render one of the following to show the matched pages that exist in - [Slot](/docs/components-Slot): Directly show sub-route with no frame. - [Stack](/docs/components-Stack): Creates a [React Navigation Stack](https://reactnavigation.org/docs/stack-navigator/) of sub-routes. - [Tabs](/docs/components-Tabs): Creates a [React Navigation BottomTabs](https://reactnavigation.org/docs/bottom-tab-navigator) with sub-routes. +- [NativeTabs](/docs/components-NativeTabs): Creates a native platform tab bar using [react-native-bottom-tabs](https://github.com/okwasniewski/react-native-bottom-tabs) with sub-routes. - [Drawer](/docs/components-Drawer): Creates a [React Navigation Drawer](https://reactnavigation.org/docs/drawer-navigator) with sub-routes. This looks something like this at the simplest: diff --git a/apps/onestack.dev/features/docs/docsRoutes.tsx b/apps/onestack.dev/features/docs/docsRoutes.tsx index 7667249fe4..c03bdcb918 100644 --- a/apps/onestack.dev/features/docs/docsRoutes.tsx +++ b/apps/onestack.dev/features/docs/docsRoutes.tsx @@ -41,6 +41,7 @@ export const docsRoutes = [ { title: 'Slot', route: '/docs/components-Slot' }, { title: 'Stack', route: '/docs/components-Stack' }, { title: 'Tabs', route: '/docs/components-Tabs' }, + { title: 'NativeTabs', route: '/docs/components-NativeTabs' }, { title: 'Drawer', route: '/docs/components-Drawer' }, { title: 'Protected', route: '/docs/components-Protected' }, { title: 'withLayoutContext', route: '/docs/exports-withLayoutContext' }, diff --git a/bun.lock b/bun.lock index 92ae67138e..a64dcbf8d3 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "vxrn-monorepo", @@ -395,6 +394,7 @@ "xxhashjs": "^0.2.2", }, "devDependencies": { + "@bottom-tabs/react-navigation": "^0.9.1", "@react-navigation/core": "^7.13.0", "@react-navigation/drawer": "~7.7.2", "@react-navigation/native": "~7.1.19", @@ -405,6 +405,7 @@ "depcheck": "^1.4.7", "immer": "^10.1.1", "react-native": "0.81.5", + "react-native-bottom-tabs": "^1.0.2", "react-native-gesture-handler": "~2.28.0", "react-native-reanimated": "~4.1.3", "rollup": "^4.29.1", @@ -413,9 +414,11 @@ "vitest": "^4.0.6", }, "peerDependencies": { + "@bottom-tabs/react-navigation": ">=0.9.0", "@react-navigation/drawer": "~7.7.2", "@react-navigation/native": "~7.1.0", "react-native": "*", + "react-native-bottom-tabs": ">=1.0.0", "react-native-gesture-handler": "~2.28.0", "react-native-reanimated": "~4.1.3", "react-native-safe-area-context": "~5.6.1", @@ -423,8 +426,10 @@ "sharp": ">=0.33.0", }, "optionalPeers": [ + "@bottom-tabs/react-navigation", "@react-navigation/drawer", "react-native", + "react-native-bottom-tabs", "react-native-gesture-handler", "react-native-reanimated", "sharp", diff --git a/examples/testflight/assets/icons/bell.svg b/examples/testflight/assets/icons/bell.svg new file mode 100644 index 0000000000..70ea70041e --- /dev/null +++ b/examples/testflight/assets/icons/bell.svg @@ -0,0 +1 @@ + diff --git a/examples/testflight/assets/icons/newspaper.svg b/examples/testflight/assets/icons/newspaper.svg new file mode 100644 index 0000000000..06d87752c2 --- /dev/null +++ b/examples/testflight/assets/icons/newspaper.svg @@ -0,0 +1 @@ + diff --git a/examples/testflight/assets/icons/user.svg b/examples/testflight/assets/icons/user.svg new file mode 100644 index 0000000000..10588f7f10 --- /dev/null +++ b/examples/testflight/assets/icons/user.svg @@ -0,0 +1 @@ + diff --git a/examples/testflight/code/home/HomeLayout.native.tsx b/examples/testflight/code/home/HomeLayout.native.tsx index c3d0846262..2f40242108 100644 --- a/examples/testflight/code/home/HomeLayout.native.tsx +++ b/examples/testflight/code/home/HomeLayout.native.tsx @@ -1,4 +1,10 @@ +import { Platform } from 'react-native' import { NativeTabs } from '~/code/ui/BottomTabs.native' +import newspaperIcon from '../../assets/icons/newspaper.svg' +import bellIcon from '../../assets/icons/bell.svg' +import userIcon from '../../assets/icons/user.svg' + +const isIOS = Platform.OS === 'ios' export function HomeLayout() { return ( @@ -7,7 +13,8 @@ export function HomeLayout() { name="index" options={{ title: 'Feed', - tabBarIcon: () => ({ sfSymbol: 'newspaper' }), + tabBarIcon: () => + isIOS ? { sfSymbol: 'newspaper' } : newspaperIcon, }} /> @@ -15,7 +22,8 @@ export function HomeLayout() { name="notifications" options={{ title: 'Notifications', - tabBarIcon: () => ({ sfSymbol: 'bell' }), + tabBarIcon: () => + isIOS ? { sfSymbol: 'bell' } : bellIcon, }} /> @@ -23,7 +31,8 @@ export function HomeLayout() { name="profile" options={{ title: 'Profile', - tabBarIcon: () => ({ sfSymbol: 'person' }), + tabBarIcon: () => + isIOS ? { sfSymbol: 'person' } : userIcon, }} /> diff --git a/examples/testflight/code/ui/BottomTabs.native.tsx b/examples/testflight/code/ui/BottomTabs.native.tsx index 64d96a9445..ae1fe94038 100644 --- a/examples/testflight/code/ui/BottomTabs.native.tsx +++ b/examples/testflight/code/ui/BottomTabs.native.tsx @@ -1,4 +1 @@ -import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation' -import { withLayoutContext } from 'one' - -export const NativeTabs = withLayoutContext(createNativeBottomTabNavigator().Navigator) +export { NativeTabs } from 'one/native-tabs' diff --git a/packages/one/package.json b/packages/one/package.json index eb17fb0180..ad3df28235 100644 --- a/packages/one/package.json +++ b/packages/one/package.json @@ -106,6 +106,15 @@ "import": "./dist/esm/drawer.mjs", "require": "./dist/cjs/drawer.js" }, + "./native-tabs": { + "react-native": { + "import": "./dist/esm/native-tabs.native.js", + "require": "./dist/cjs/native-tabs.native.js" + }, + "types": "./types/native-tabs.d.ts", + "import": "./dist/esm/native-tabs.mjs", + "require": "./dist/cjs/native-tabs.js" + }, "./devtools/*": "./devtools/*" }, "main": "dist/cjs", @@ -201,9 +210,17 @@ "react-native-reanimated": "~4.1.3", "react-native-safe-area-context": "~5.6.1", "react-native-screens": "~4.16.0", + "@bottom-tabs/react-navigation": ">=0.9.0", + "react-native-bottom-tabs": ">=1.0.0", "sharp": ">=0.33.0" }, "peerDependenciesMeta": { + "@bottom-tabs/react-navigation": { + "optional": true + }, + "react-native-bottom-tabs": { + "optional": true + }, "@react-navigation/drawer": { "optional": true }, @@ -221,6 +238,8 @@ } }, "devDependencies": { + "@bottom-tabs/react-navigation": "^0.9.1", + "react-native-bottom-tabs": "^1.0.2", "@react-navigation/core": "^7.13.0", "@react-navigation/drawer": "~7.7.2", "@react-navigation/native": "~7.1.19", diff --git a/packages/one/src/layouts/NativeTabs.tsx b/packages/one/src/layouts/NativeTabs.tsx new file mode 100644 index 0000000000..c89bb8b9a9 --- /dev/null +++ b/packages/one/src/layouts/NativeTabs.tsx @@ -0,0 +1,24 @@ +import { + createNativeBottomTabNavigator, + type NativeBottomTabNavigationEventMap, + type NativeBottomTabNavigationOptions, +} from '@bottom-tabs/react-navigation' +import type { ParamListBase, TabNavigationState } from '@react-navigation/native' +import type React from 'react' + +import { withLayoutContext } from './withLayoutContext' + +// typed as ComponentType because @bottom-tabs/react-navigation doesn't export +// NativeBottomTabNavigatorProps, causing TS2742 on build. the actual typing comes +// from the withLayoutContext generics below, not from the Navigator's own props. +const NativeBottomTabNavigator: React.ComponentType = + createNativeBottomTabNavigator().Navigator + +export const NativeTabs = withLayoutContext< + NativeBottomTabNavigationOptions, + typeof NativeBottomTabNavigator, + TabNavigationState, + NativeBottomTabNavigationEventMap +>(NativeBottomTabNavigator) + +export default NativeTabs diff --git a/packages/one/src/native-tabs.ts b/packages/one/src/native-tabs.ts new file mode 100644 index 0000000000..9c157ac44a --- /dev/null +++ b/packages/one/src/native-tabs.ts @@ -0,0 +1 @@ +export { NativeTabs, NativeTabs as default } from './layouts/NativeTabs' diff --git a/packages/one/types/layouts/NativeTabs.d.ts b/packages/one/types/layouts/NativeTabs.d.ts new file mode 100644 index 0000000000..bf7a691b2a --- /dev/null +++ b/packages/one/types/layouts/NativeTabs.d.ts @@ -0,0 +1,6 @@ +import type React from 'react'; +export declare const NativeTabs: React.ForwardRefExoticComponent, "ref"> & React.RefAttributes> & { + Screen: typeof import("../views/Screen").Screen; +}; +export default NativeTabs; +//# sourceMappingURL=NativeTabs.d.ts.map \ No newline at end of file diff --git a/packages/one/types/native-tabs.d.ts b/packages/one/types/native-tabs.d.ts new file mode 100644 index 0000000000..6f3e27aa3c --- /dev/null +++ b/packages/one/types/native-tabs.d.ts @@ -0,0 +1,2 @@ +export { NativeTabs, NativeTabs as default } from './layouts/NativeTabs'; +//# sourceMappingURL=native-tabs.d.ts.map \ No newline at end of file