diff --git a/app/(app)/(tabs)/_layout.tsx b/app/(app)/(tabs)/_layout.tsx
index 84c8ebe8..4209b19f 100644
--- a/app/(app)/(tabs)/_layout.tsx
+++ b/app/(app)/(tabs)/_layout.tsx
@@ -1,53 +1,18 @@
-import { Redirect, Tabs } from 'expo-router'
+import { Redirect } from 'expo-router'
-import { Icon } from '~components'
-import { useAuth, useNavigationTheme } from '~hooks'
-import { IconNames } from '~types/icon'
+import { useAuth } from '~hooks'
+import { ResponsiveNavigator } from '~navigation/tabNavigator/navigator'
export const unstable_settings = {
initialRouteName: 'home',
}
-function TabBarIcon(props: { color: ColorNames; name: IconNames }) {
- return
-}
-
export default function TabLayout() {
- const { tabBarTheme } = useNavigationTheme()
const { isSignedIn } = useAuth()
if (isSignedIn === false) {
return
}
- return (
- null,
- }}
- >
- ,
- }}
- />
- ,
- }}
- />
- ,
- }}
- />
-
- )
+ return
}
diff --git a/assets/logo/logo-full-dark.png b/assets/logo/logo-full-dark.png
new file mode 100644
index 00000000..a14668ad
Binary files /dev/null and b/assets/logo/logo-full-dark.png differ
diff --git a/assets/logo/logo-full-light.png b/assets/logo/logo-full-light.png
new file mode 100644
index 00000000..5f767a5a
Binary files /dev/null and b/assets/logo/logo-full-light.png differ
diff --git a/assets/logo/logo-sygnet-dark.png b/assets/logo/logo-sygnet-dark.png
new file mode 100644
index 00000000..a64f209b
Binary files /dev/null and b/assets/logo/logo-sygnet-dark.png differ
diff --git a/assets/logo/logo-sygnet-light.png b/assets/logo/logo-sygnet-light.png
new file mode 100644
index 00000000..eaba3433
Binary files /dev/null and b/assets/logo/logo-sygnet-light.png differ
diff --git a/declaration.d.ts b/declaration.d.ts
new file mode 100644
index 00000000..acd2815e
--- /dev/null
+++ b/declaration.d.ts
@@ -0,0 +1,5 @@
+// declaration.d.ts
+declare module '*.scss' {
+ const content: Record
+ export default content
+}
diff --git a/package.json b/package.json
index 0ce4922f..a9cc6573 100644
--- a/package.json
+++ b/package.json
@@ -86,6 +86,7 @@
"whoami": "expo whoami"
},
"dependencies": {
+ "@bacons/react-views": "^1.1.3",
"@expo/config-plugins": "~7.8.0",
"@expo/prebuild-config": "~6.7.0",
"@expo/vector-icons": "^14.0.0",
@@ -142,6 +143,7 @@
"react-native-svg": "14.1.0",
"react-native-web": "~0.19.6",
"reactotron-react-native": "^5.0.3",
+ "sass": "^1.70.0",
"setimmediate": "^1.0.5",
"use-debounce": "^9.0.4"
},
diff --git a/src/components/README.md b/src/components/README.md
index 96e9044a..815855c1 100644
--- a/src/components/README.md
+++ b/src/components/README.md
@@ -61,9 +61,9 @@ import { Input, AbsoluteFullFill } from '~components'
const MyComponent: React.FC = () => (
@@ -107,18 +107,18 @@ import { Spacer, Input, Container } from '~components'
const MyComponent: React.FC = () => (
@@ -195,12 +195,12 @@ import { Field } from '~components'
const MyComponent: React.FC = () => (
)
@@ -285,16 +285,15 @@ const MyComponent: React.FC = () => {
return (
(
const handleFocus = useCallback(
(e?: NativeSyntheticEvent) => {
+ if (isDisabled) return
_inputRef.current?.focus()
setIsFocused(true)
if (onFocus && e) onFocus(e)
},
- [setIsFocused, onFocus]
+ [isDisabled, onFocus]
)
const handleBlur = useCallback(
@@ -236,7 +243,10 @@ export const Input = forwardRef(
autoCapitalize="none"
color={isInvalid ? 'danger' : 'text'}
cursorColor={colors.primaryLight}
- editable={!isDisabled}
+ {...Platform.select({
+ default: { editable: !isDisabled },
+ web: { disabled: isDisabled },
+ })}
flex={1}
fontFamily="regular"
fontSize="xs"
diff --git a/src/components/molecules/Field/Input.tsx b/src/components/molecules/Field/Input.tsx
index 21c35918..b0e9186e 100644
--- a/src/components/molecules/Field/Input.tsx
+++ b/src/components/molecules/Field/Input.tsx
@@ -34,7 +34,6 @@ const layoutPropsKeys = [
export const Input = forwardRef, FieldInputProps>(
(
{
- isDisabled,
isRequired,
isInvalid,
label,
diff --git a/src/constants/images.ts b/src/constants/images.ts
index e8414bd7..fa14b904 100644
--- a/src/constants/images.ts
+++ b/src/constants/images.ts
@@ -1,3 +1,9 @@
/* eslint-disable @typescript-eslint/no-var-requires */
export const darkLogo = require('~assets/logo-dark.png')
export const lightLogo = require('~assets/logo-light.png')
+
+export const darkLogoFull = require('~assets/logo/logo-full-dark.png')
+export const lightLogoFull = require('~assets/logo/logo-full-light.png')
+
+export const darkLogoSygnet = require('~assets/logo/logo-sygnet-dark.png')
+export const lightLogoSygnet = require('~assets/logo/logo-sygnet-light.png')
diff --git a/src/hooks/navigation/useNavigationStatePersistence.ts b/src/hooks/navigation/useNavigationStatePersistence.ts
index 1f735f5b..88cf54ac 100644
--- a/src/hooks/navigation/useNavigationStatePersistence.ts
+++ b/src/hooks/navigation/useNavigationStatePersistence.ts
@@ -20,6 +20,7 @@ type NavigationStatePersistenceReturn = {
const { NAVIGATION_STATE } = ASYNC_STORAGE_KEYS
+// TODO: make this work on expo router
export const useNavigationStatePersistence = (): NavigationStatePersistenceReturn => {
const [isReady, setIsReady] = useState(isProduction)
const [initialState, setInitialState] = useState()
diff --git a/src/hooks/navigation/useScreenTracker.ts b/src/hooks/navigation/useScreenTracker.ts
index d2761201..a88695fe 100644
--- a/src/hooks/navigation/useScreenTracker.ts
+++ b/src/hooks/navigation/useScreenTracker.ts
@@ -16,6 +16,7 @@ type ScreenTrackerReturn = {
onStateChange: () => Promise
}
+// TODO: make this work on expo router
export const useScreenTracker = (callback = defaultCallback): ScreenTrackerReturn => {
const routeNameRef = useRef()
diff --git a/src/hooks/navigation/useWeb.ts b/src/hooks/navigation/useWeb.ts
index bebff2a5..44f85d1e 100644
--- a/src/hooks/navigation/useWeb.ts
+++ b/src/hooks/navigation/useWeb.ts
@@ -13,8 +13,6 @@ const { desktop, tablet } = breakpoints
export const useWeb: () => ReturnType = () => {
const [width, setWidth] = useState(0)
- console.log('useWeb', width)
-
useEffect(() => {
const setDimensions = (windowWidth: number, screenWidth: number) => {
switch (true) {
diff --git a/src/navigation/tabNavigator/components/AppHeader.tsx b/src/navigation/tabNavigator/components/AppHeader.tsx
new file mode 100644
index 00000000..5f3eef5e
--- /dev/null
+++ b/src/navigation/tabNavigator/components/AppHeader.tsx
@@ -0,0 +1,44 @@
+import { Image, Platform, StyleSheet, View } from 'react-native'
+import { useSafeAreaInsets } from 'react-native-safe-area-context'
+
+import { TabColorsStrings } from '../config'
+
+import { darkLogoFull, lightLogoFull } from '~constants'
+import { useColorScheme } from '~contexts'
+
+export function AppHeader() {
+ const { colorScheme } = useColorScheme()
+ const { top } = useSafeAreaInsets()
+
+ const height = 60 + top
+ return (
+
+
+
+ )
+}
+
+const jsStyles = StyleSheet.create({
+ appHeader: {
+ alignItems: 'center',
+ borderBottomColor: TabColorsStrings.lightGray,
+ borderBottomWidth: 1,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ paddingHorizontal: 16,
+ width: '100%',
+ zIndex: 10,
+ },
+ logoWide: { height: 60, width: 150 },
+})
diff --git a/src/navigation/tabNavigator/components/BottomBar.tsx b/src/navigation/tabNavigator/components/BottomBar.tsx
new file mode 100644
index 00000000..206bc3bb
--- /dev/null
+++ b/src/navigation/tabNavigator/components/BottomBar.tsx
@@ -0,0 +1,73 @@
+import { Platform, StyleSheet, View } from 'react-native'
+import { useSafeAreaInsets } from 'react-native-safe-area-context'
+
+import { TabBarItemWrapper } from './TabBarItemWrapper'
+import { bottomTabs, TabColors, TabColorsStrings } from '../config'
+import { cns } from '../utils'
+
+import { Icon } from '~components'
+import { useColorScheme } from '~contexts'
+import cssStyles from '~styles'
+
+export function BottomBar({ visible }: { visible: boolean }) {
+ const { colorScheme } = useColorScheme()
+ return (
+
+
+ {bottomTabs.map((tab, i) => (
+
+ {({ focused, pressed, hovered }) => (
+
+ )}
+
+ ))}
+
+
+ )
+}
+
+const jsStyles = StyleSheet.create({
+ nav: {
+ alignItems: 'center',
+ borderTopColor: TabColorsStrings.lightGray,
+ borderTopWidth: 1,
+ flexDirection: 'row',
+ height: 49,
+ justifyContent: 'space-around',
+ paddingHorizontal: 16,
+ },
+
+ tabIcon: {
+ paddingHorizontal: 8,
+ },
+ tabIconPressed: {
+ opacity: 0.8,
+ transform: [{ scale: 0.9 }],
+ },
+})
diff --git a/src/navigation/tabNavigator/components/HeaderLogo.tsx b/src/navigation/tabNavigator/components/HeaderLogo.tsx
new file mode 100644
index 00000000..c7e6d338
--- /dev/null
+++ b/src/navigation/tabNavigator/components/HeaderLogo.tsx
@@ -0,0 +1,93 @@
+import { Pressable, Text } from '@bacons/react-views'
+import { Link } from 'expo-router'
+import { Image, Platform, StyleSheet, View } from 'react-native'
+
+import { TabColorsStrings } from '../config'
+import { useWidth } from '../hooks'
+import { cns } from '../utils'
+
+import { darkLogoFull, darkLogoSygnet, lightLogoFull, lightLogoSygnet } from '~constants'
+import { useColorScheme } from '~contexts'
+import cssStyles from '~styles'
+
+export function HeaderLogo() {
+ const { colorScheme } = useColorScheme()
+ const isLargeHorizontal = useWidth(1264)
+
+ return (
+
+
+
+ {({ hovered }) => (
+
+
+
+
+ )}
+
+
+
+ )
+}
+
+const jsStyles = StyleSheet.create({
+ headerContainer: {
+ height: 96,
+ minHeight: 96,
+ paddingTop: 0,
+ },
+ headerLink: {
+ alignItems: 'center',
+ },
+ headerLogo: {
+ alignItems: 'center',
+ borderRadius: 8,
+ display: 'flex',
+ margin: 0,
+ },
+ logoSygnet: { height: 60, width: 40 },
+ logoWide: { height: 60, width: 150 },
+})
diff --git a/src/navigation/tabNavigator/components/SideBar.tsx b/src/navigation/tabNavigator/components/SideBar.tsx
new file mode 100644
index 00000000..8c480e91
--- /dev/null
+++ b/src/navigation/tabNavigator/components/SideBar.tsx
@@ -0,0 +1,121 @@
+import { Platform, StyleSheet, View } from 'react-native'
+
+import { HeaderLogo } from './HeaderLogo'
+import { SideBarTabItem } from './SideBarTabItem'
+import { TabColorsStrings, upperSideTabs } from '../config'
+import { useWidth } from '../hooks'
+import { cns } from '../utils'
+
+import { useAuth } from '~hooks'
+import cssStyles from '~styles'
+
+const NAV_MEDIUM_WIDTH = 244
+
+export function SideBar({ visible }: { visible: boolean }) {
+ const isLarge = useWidth(1264)
+ const { signOut } = useAuth()
+
+ return (
+
+
+
+
+
+
+ {upperSideTabs.map((tab) => (
+
+ {tab.displayedName}
+
+ ))}
+
+
+
+ Logout
+
+
+
+
+
+ )
+}
+
+const jsStyles = StyleSheet.create({
+ sideBar: {
+ minWidth: 72,
+ width: 72,
+ },
+
+ sidebarInner: {
+ alignItems: 'stretch',
+ borderRightColor: TabColorsStrings.lightGray,
+ borderRightWidth: 1,
+ height: '100%',
+ maxHeight: '100%',
+ minWidth: 72,
+ paddingBottom: 20,
+ paddingHorizontal: 12,
+ paddingTop: 8,
+ position: 'absolute',
+ width: 72,
+ },
+ sidebarInner2: {
+ alignItems: 'stretch',
+ flex: 1,
+ height: '100%',
+ justifyContent: 'space-between',
+ },
+
+ sidebarTabs: { flex: 1, gap: 4 },
+})
diff --git a/src/navigation/tabNavigator/components/SideBarTabItem.tsx b/src/navigation/tabNavigator/components/SideBarTabItem.tsx
new file mode 100644
index 00000000..e656e47e
--- /dev/null
+++ b/src/navigation/tabNavigator/components/SideBarTabItem.tsx
@@ -0,0 +1,116 @@
+import { Text } from '@bacons/react-views'
+import { Platform, StyleSheet, View } from 'react-native'
+
+import { TabBarItemWrapper } from './TabBarItemWrapper'
+import { TabColors, TabColorsStrings } from '../config'
+import { useWidth } from '../hooks'
+import { cns } from '../utils'
+
+import { Icon } from '~components'
+import { useColorScheme } from '~contexts'
+import cssStyles from '~styles'
+import { IconNames } from '~types/icon'
+
+export function SideBarTabItem({
+ children,
+ icon,
+ iconFocused,
+ name,
+ onPress,
+ params,
+}: {
+ children: string
+ icon: IconNames
+ iconFocused: IconNames
+ name: string
+ onPress?(): void
+ params?: Record
+}) {
+ const isLarge = useWidth(1264)
+ const { colorScheme } = useColorScheme()
+
+ return (
+
+ {({ focused, hovered }) => (
+
+
+
+
+
+
+ {children}
+
+
+ )}
+
+ )
+}
+
+const jsStyles = StyleSheet.create({
+ fontBold: { fontWeight: 'bold' },
+ sidebarIconContainer: Platform.select({
+ default: { padding: 0 },
+ web: {
+ transitionDuration: '150ms',
+ transitionProperty: ['transform'],
+ transitionTimingFunction: 'cubic-bezier(0.17, 0.17, 0, 1)',
+ },
+ }),
+ sidebarItemContainer: {
+ alignItems: 'center',
+ borderRadius: 8,
+ flexDirection: 'row',
+ padding: 8,
+ ...Platform.select({
+ web: {
+ transitionDuration: '200ms',
+ transitionProperty: ['background-color', 'box-shadow'],
+ },
+ }),
+ },
+ sidebarItemText: {
+ fontSize: 16,
+ lineHeight: 24,
+ marginLeft: 16,
+ marginRight: 16,
+ },
+ sidebarTabItem: {
+ paddingVertical: 4,
+ width: '100%',
+ },
+})
diff --git a/src/navigation/tabNavigator/components/TabBarItemWrapper.tsx b/src/navigation/tabNavigator/components/TabBarItemWrapper.tsx
new file mode 100644
index 00000000..2ac29c1e
--- /dev/null
+++ b/src/navigation/tabNavigator/components/TabBarItemWrapper.tsx
@@ -0,0 +1,43 @@
+import { Pressable } from '@bacons/react-views'
+import { Link } from 'expo-router'
+import { PressableStateCallbackType, ViewStyle } from 'react-native'
+
+import { useIsTabSelected } from '../hooks'
+import { TabbedNavigator } from '../tab-slot'
+
+export function TabBarItemWrapper({
+ children,
+ id,
+ name,
+ onPress,
+ params,
+ style,
+}: {
+ children?: (
+ props: PressableStateCallbackType & { hovered: boolean; focused: boolean }
+ ) => JSX.Element
+ id: string
+ name: string
+ onPress?: () => void
+ params?: Record
+ style?: ViewStyle
+}) {
+ const focused = useIsTabSelected(id)
+
+ if (onPress) {
+ return {(props) => children?.({ ...props, focused })}
+ }
+
+ if (name.startsWith('/') || name.startsWith('.')) {
+ return (
+
+ {(props) => children?.({ ...props, focused })}
+
+ )
+ }
+ return (
+
+ {(props) => children?.({ ...props, focused })}
+
+ )
+}
diff --git a/src/navigation/tabNavigator/components/index.ts b/src/navigation/tabNavigator/components/index.ts
new file mode 100644
index 00000000..29d34def
--- /dev/null
+++ b/src/navigation/tabNavigator/components/index.ts
@@ -0,0 +1,3 @@
+export * from './AppHeader'
+export * from './BottomBar'
+export * from './SideBar'
diff --git a/src/navigation/tabNavigator/config.ts b/src/navigation/tabNavigator/config.ts
new file mode 100644
index 00000000..5903b051
--- /dev/null
+++ b/src/navigation/tabNavigator/config.ts
@@ -0,0 +1,63 @@
+import { palette } from '~constants'
+import { IconNames } from '~types/icon'
+import { hex2rgba } from '~utils'
+
+type Tab = {
+ displayedName: string
+ icon: IconNames
+ iconFocused: IconNames
+ id: string
+ name: string
+ params?: Record
+}
+type Tabs = Tab[]
+
+// name with '/' at the begging will not be resolved as 'bottom tab', will be as usual screen
+export const upperSideTabs: Tabs = [
+ {
+ displayedName: 'Home',
+ icon: 'home-3-line',
+ iconFocused: 'home-3-fill',
+ id: 'home',
+ name: 'home',
+ },
+ {
+ displayedName: 'Example',
+ icon: 'aliens-line',
+ iconFocused: 'aliens-fill',
+ id: 'example',
+ name: 'example',
+ },
+ {
+ displayedName: 'Settings',
+ icon: 'settings-2-line',
+ iconFocused: 'settings-2-fill',
+ id: 'settings',
+ name: 'settings',
+ },
+ {
+ displayedName: 'Details',
+ icon: 'baidu-line',
+ iconFocused: 'baidu-fill',
+ id: 'details',
+ name: '/home/details',
+ params: { user: 'example@test.com' },
+ },
+]
+
+export const bottomSideTabs: Tabs = []
+
+export const bottomTabs: Tabs = [...upperSideTabs]
+
+export const TabColors: Record = {
+ tabIconDark: 'gray.700',
+ tabIconLight: 'gray.200',
+} as const
+
+export const TabColorsStrings = {
+ lightGray: palette.gray['300'],
+ lightGray50: hex2rgba(palette.gray['50'], 0.5),
+ tabTextDark: palette.gray['700'],
+ tabTextLight: palette.gray['300'],
+ transparent: 'transparent',
+} as const
diff --git a/src/navigation/tabNavigator/hooks/index.ts b/src/navigation/tabNavigator/hooks/index.ts
new file mode 100644
index 00000000..a84485b9
--- /dev/null
+++ b/src/navigation/tabNavigator/hooks/index.ts
@@ -0,0 +1,5 @@
+export * from './useIsTabSelected'
+export * from './useWidth'
+export * from './useContextRoute'
+export * from './useLinkBuilder'
+export * from './useNavigatorContext'
diff --git a/src/navigation/tabNavigator/hooks/useContextRoute.ts b/src/navigation/tabNavigator/hooks/useContextRoute.ts
new file mode 100644
index 00000000..4d3e206a
--- /dev/null
+++ b/src/navigation/tabNavigator/hooks/useContextRoute.ts
@@ -0,0 +1,30 @@
+import { Navigator } from 'expo-router'
+
+export function useContextRoute(name: string) {
+ const context = Navigator.useContext()
+
+ const { state, navigation, descriptors } = context
+
+ const route = state.routes.find((route) => {
+ return route.name === name
+ })
+
+ if (!route) {
+ console.warn(
+ `Could not find route with name: ${name}. Options: ${state.routes
+ .map((r) => r.name)
+ .join(', ')}`
+ )
+ }
+
+ if (!route) {
+ return null
+ }
+
+ return {
+ route,
+ target: state.key,
+ navigation,
+ descriptor: descriptors[route.key],
+ }
+}
diff --git a/src/navigation/tabNavigator/hooks/useIsTabSelected.ts b/src/navigation/tabNavigator/hooks/useIsTabSelected.ts
new file mode 100644
index 00000000..8fc52cdc
--- /dev/null
+++ b/src/navigation/tabNavigator/hooks/useIsTabSelected.ts
@@ -0,0 +1,10 @@
+import { useNavigatorContext } from './useNavigatorContext'
+
+export function useIsTabSelected(name: string): boolean {
+ const { navigation } = useNavigatorContext()
+
+ const state = navigation.getState()
+ const current = state.routes.find((_, i) => state.index === i)
+
+ return current?.name === name
+}
diff --git a/src/navigation/tabNavigator/hooks/useLinkBuilder.tsx b/src/navigation/tabNavigator/hooks/useLinkBuilder.tsx
new file mode 100644
index 00000000..79138fe0
--- /dev/null
+++ b/src/navigation/tabNavigator/hooks/useLinkBuilder.tsx
@@ -0,0 +1,70 @@
+import {
+ NavigationHelpers,
+ NavigationHelpersContext,
+ NavigationProp,
+ ParamListBase,
+} from '@react-navigation/core'
+import { LinkingContext } from '@react-navigation/native'
+import * as React from 'react'
+
+type NavigationObject = NavigationHelpers | NavigationProp
+
+type MinimalState = {
+ index: number
+ routes: { name: string; params?: object; state?: MinimalState }[]
+}
+
+const getRootStateForNavigate = (
+ navigation: NavigationObject,
+ state: MinimalState
+): MinimalState => {
+ const parent = navigation.getParent()
+
+ if (parent) {
+ const parentState = parent.getState()
+
+ return getRootStateForNavigate(parent, {
+ index: 0,
+ routes: [
+ {
+ ...parentState.routes[parentState.index],
+ state,
+ },
+ ],
+ })
+ }
+
+ return state
+}
+
+/**
+ * Build destination link for a navigate action.
+ * Useful for showing anchor tags on the web for buttons that perform navigation.
+ */
+export function useLinkBuilder() {
+ const navigation = React.useContext(NavigationHelpersContext)
+ const linking = React.useContext(LinkingContext)
+
+ const buildLink = React.useCallback(
+ (name: string, params?: object) => {
+ const state = navigation
+ ? getRootStateForNavigate(navigation, {
+ index: 0,
+ routes: [{ name, params }],
+ })
+ : // If we couldn't find a navigation object in context, we're at root
+ // So we'll construct a basic state object to use
+ {
+ index: 0,
+ routes: [{ name, params }],
+ }
+
+ const out = linking.options!.getPathFromState?.(state, linking.options!.config)
+
+ return out
+ },
+ [linking, navigation]
+ )
+
+ return buildLink
+}
diff --git a/src/navigation/tabNavigator/hooks/useNavigatorContext.ts b/src/navigation/tabNavigator/hooks/useNavigatorContext.ts
new file mode 100644
index 00000000..9b9204a8
--- /dev/null
+++ b/src/navigation/tabNavigator/hooks/useNavigatorContext.ts
@@ -0,0 +1,16 @@
+import { TabRouter } from '@react-navigation/routers'
+import { Navigator } from 'expo-router'
+
+export function useNavigatorContext() {
+ const context = Navigator.useContext()
+
+ if (process.env.NODE_ENV !== 'production') {
+ if (!(context.router.name === 'TabRouter' || context.router instanceof TabRouter)) {
+ throw new Error(
+ 'useTabbedSlot must be used inside a Navigator with a tab router: '
+ )
+ }
+ }
+
+ return context
+}
diff --git a/src/navigation/tabNavigator/hooks/useWidth.ts b/src/navigation/tabNavigator/hooks/useWidth.ts
new file mode 100644
index 00000000..093b9fcb
--- /dev/null
+++ b/src/navigation/tabNavigator/hooks/useWidth.ts
@@ -0,0 +1,12 @@
+import { Platform, useWindowDimensions } from 'react-native'
+
+export function useWidth(size: number): boolean {
+ const { width } = useWindowDimensions()
+ if (typeof window === 'undefined') {
+ return true
+ }
+ if (Platform.OS === 'ios' || Platform.OS === 'android') {
+ return false
+ }
+ return width >= size
+}
diff --git a/src/navigation/tabNavigator/navigator.tsx b/src/navigation/tabNavigator/navigator.tsx
new file mode 100644
index 00000000..9af562c9
--- /dev/null
+++ b/src/navigation/tabNavigator/navigator.tsx
@@ -0,0 +1,45 @@
+import { StyleSheet } from '@bacons/react-views'
+import React from 'react'
+import { Platform, View } from 'react-native'
+
+import { AppHeader, BottomBar, SideBar } from './components'
+import { useWidth } from './hooks'
+import { TabbedNavigator } from './tab-slot'
+import { cns } from './utils'
+
+import cssStyles from '~styles'
+
+export function ResponsiveNavigator() {
+ const isRowLayout = useWidth(768)
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const jsStyles = StyleSheet.create({
+ flex1: { flex: 1 },
+ flexGrow1: { flexGrow: 1 },
+})
diff --git a/src/navigation/tabNavigator/tab-slot.tsx b/src/navigation/tabNavigator/tab-slot.tsx
new file mode 100644
index 00000000..c53a2490
--- /dev/null
+++ b/src/navigation/tabNavigator/tab-slot.tsx
@@ -0,0 +1,136 @@
+// Like from Expo Router but with stored tab history.
+import { CommonActions } from '@react-navigation/native'
+import { TabRouter } from '@react-navigation/routers'
+import { Link, Navigator } from 'expo-router'
+import { Screen as RouterScreen } from 'expo-router/build/views/Screen'
+import * as React from 'react'
+import { GestureResponderEvent, StyleSheet, ViewStyle } from 'react-native'
+import { Screen, ScreenContainer } from 'react-native-screens'
+
+import { useContextRoute, useLinkBuilder, useNavigatorContext } from './hooks'
+
+export function TabbedNavigator(props: React.ComponentProps) {
+ return
+}
+
+export default function TabbedSlot({
+ detachInactiveScreens = true,
+ style,
+}: {
+ detachInactiveScreens?: boolean
+ style?: ViewStyle
+}) {
+ const { state, descriptors } = useNavigatorContext()
+ const focusedRouteKey = state.routes[state.index].key
+ const [loaded, setLoaded] = React.useState([focusedRouteKey])
+
+ if (!loaded.includes(focusedRouteKey)) {
+ setLoaded([...loaded, focusedRouteKey])
+ }
+
+ const { routes } = state
+
+ return (
+
+ {routes.map((route, index) => {
+ const descriptor = descriptors[route.key]
+ const {
+ freezeOnBlur,
+ lazy = true,
+ unmountOnBlur,
+ } = descriptor.options as unknown as {
+ lazy: boolean
+ unmountOnBlur?: boolean
+ freezeOnBlur: boolean
+ }
+ const isFocused = state.index === index
+
+ if (unmountOnBlur && !isFocused) {
+ return null
+ }
+
+ if (lazy && !loaded.includes(route.key) && !isFocused) {
+ // Don't render a lazy screen if we've never navigated to it
+ return null
+ }
+
+ const zIndex = { zIndex: isFocused ? 0 : -1 }
+
+ return (
+
+ {descriptor.render()}
+
+ )
+ })}
+
+ )
+}
+
+export function TabLink({
+ name,
+ params,
+ ...props
+}: { name: string; params?: Record } & Omit<
+ React.ComponentProps,
+ 'href' | 'onPress' | 'onLongPress'
+>) {
+ const buildLink = useLinkBuilder()
+ const ctxRoute = useContextRoute(name)
+
+ if (!ctxRoute) {
+ return null
+ }
+
+ const { route, target, navigation } = ctxRoute
+
+ const onPress = (
+ e: GestureResponderEvent | React.MouseEvent
+ ) => {
+ const event = navigation.emit({
+ type: 'tabPress',
+ target: route.key,
+ canPreventDefault: true,
+ })
+
+ // @ts-expect-error: event type does not contain defaultPrevented, which should be available here on web
+ if (!event.defaultPrevented) {
+ e.preventDefault()
+ navigation.dispatch({
+ ...CommonActions.navigate({ name: route.name, merge: true, params }),
+ target,
+ })
+ }
+ }
+
+ const onLongPress = () => {
+ navigation.emit({
+ type: 'tabLongPress',
+ target: route.key,
+ })
+ }
+
+ return
+}
+
+TabbedNavigator.Slot = TabbedSlot
+TabbedNavigator.Link = TabLink
+TabbedNavigator.Screen = RouterScreen
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ overflow: 'hidden',
+ },
+ screen: {
+ ...StyleSheet.absoluteFillObject,
+ overflow: 'hidden',
+ },
+})
diff --git a/src/navigation/tabNavigator/utils/cns.ts b/src/navigation/tabNavigator/utils/cns.ts
new file mode 100644
index 00000000..6ccb48de
--- /dev/null
+++ b/src/navigation/tabNavigator/utils/cns.ts
@@ -0,0 +1,6 @@
+export const cns = (
+ ...classes: (string | false | undefined | null)[]
+): Record => ({
+ $$css: true,
+ _: classes.filter(Boolean).join(' ') as unknown as string[],
+})
diff --git a/src/navigation/tabNavigator/utils/index.ts b/src/navigation/tabNavigator/utils/index.ts
new file mode 100644
index 00000000..817041e1
--- /dev/null
+++ b/src/navigation/tabNavigator/utils/index.ts
@@ -0,0 +1 @@
+export * from './cns'
diff --git a/src/screens/SignInScreen.tsx b/src/screens/SignInScreen.tsx
index 134d446c..aead3936 100644
--- a/src/screens/SignInScreen.tsx
+++ b/src/screens/SignInScreen.tsx
@@ -53,13 +53,13 @@ export const SignInScreen = (): JSX.Element => {
{
{
{
/>
{
name="password"
onSubmitEditing={submit}
placeholder={t('sign_in_screen.password_placeholder')}
- returnKeyType="next"
rules={{
required: t('form.required'),
}}
diff --git a/src/screens/TestFormScreen.tsx b/src/screens/TestFormScreen.tsx
index 1bec2011..f758a37c 100644
--- a/src/screens/TestFormScreen.tsx
+++ b/src/screens/TestFormScreen.tsx
@@ -74,119 +74,107 @@ export const TestFormScreen = (): JSX.Element => {
{t('test_form.contact_data')}
{t('test_form.additional_comment')}
(