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')} (