diff --git a/packages/core-mobile/app/new/common/components/BlurredBackgroundView.tsx b/packages/core-mobile/app/new/common/components/BlurredBackgroundView.tsx index 7b87d1045a..f1c0a784cf 100644 --- a/packages/core-mobile/app/new/common/components/BlurredBackgroundView.tsx +++ b/packages/core-mobile/app/new/common/components/BlurredBackgroundView.tsx @@ -1,6 +1,7 @@ import React from 'react' -import { GlassView, Separator, View } from '@avalabs/k2-alpine' +import { alpha, Separator, useTheme, View } from '@avalabs/k2-alpine' import { Platform } from 'react-native' +import { BlurView } from 'expo-blur' const BlurredBackgroundView = ({ separator @@ -10,13 +11,23 @@ const BlurredBackgroundView = ({ position: 'top' | 'bottom' } }): JSX.Element => { + const { theme } = useTheme() + return ( {separator?.position === 'top' && ( )} {Platform.OS === 'ios' ? ( - + ) : ( )} diff --git a/packages/core-mobile/app/new/common/components/SimpleTextInput.tsx b/packages/core-mobile/app/new/common/components/SimpleTextInput.tsx index 87bcc196ea..45644f8ab9 100644 --- a/packages/core-mobile/app/new/common/components/SimpleTextInput.tsx +++ b/packages/core-mobile/app/new/common/components/SimpleTextInput.tsx @@ -10,11 +10,13 @@ import { export const SimpleTextInput = ({ name, setName, - placeholder + placeholder, + maxLength }: { name: string setName: (name: string) => void placeholder?: string + maxLength?: number }): React.JSX.Element => { const { theme: { colors } @@ -23,7 +25,7 @@ export const SimpleTextInput = ({ {name.length !== 0 && ( setName('')}> diff --git a/packages/core-mobile/app/new/common/consts/screenOptions.tsx b/packages/core-mobile/app/new/common/consts/screenOptions.tsx index ab666c5c3a..f0c460097f 100644 --- a/packages/core-mobile/app/new/common/consts/screenOptions.tsx +++ b/packages/core-mobile/app/new/common/consts/screenOptions.tsx @@ -112,7 +112,7 @@ export const homeScreenOptions: StackNavigationOptions = { height: '100%', justifyContent: 'center' }}> - + diff --git a/packages/core-mobile/app/new/common/hooks/useAnimatedNavigationHeader.tsx b/packages/core-mobile/app/new/common/hooks/useFadingHeaderNavigation.tsx similarity index 52% rename from packages/core-mobile/app/new/common/hooks/useAnimatedNavigationHeader.tsx rename to packages/core-mobile/app/new/common/hooks/useFadingHeaderNavigation.tsx index f98e9af2bd..20aca3b1b9 100644 --- a/packages/core-mobile/app/new/common/hooks/useAnimatedNavigationHeader.tsx +++ b/packages/core-mobile/app/new/common/hooks/useFadingHeaderNavigation.tsx @@ -1,38 +1,62 @@ import BlurredBackgroundView from 'common/components/BlurredBackgroundView' import React, { useCallback, useEffect, useState } from 'react' import { View } from '@avalabs/k2-alpine' -import { LayoutChangeEvent, LayoutRectangle } from 'react-native' +import { + LayoutChangeEvent, + LayoutRectangle, + NativeScrollEvent, + NativeSyntheticEvent, + ScrollViewProps +} from 'react-native' import { useNavigation } from 'expo-router' +import { clamp } from 'react-native-reanimated' -export const useAnimatedNavigationHeader = ({ +export const useFadingHeaderNavigation = ({ header, - visibilityProgress + targetLayout }: { - header: JSX.Element - visibilityProgress: number -}): void => { + header?: JSX.Element + targetLayout?: LayoutRectangle +}): Partial => { const navigation = useNavigation() const [navigationHeaderLayout, setNavigationHeaderLayout] = useState< LayoutRectangle | undefined >(undefined) + const [targetHiddenProgress, setTargetHiddenProgress] = useState(0) // from 0 to 1, 0 = fully hidden, 1 = fully shown const renderHeaderBackground = useCallback( () => ( ), - [visibilityProgress] + [targetHiddenProgress] ) const handleLayout = (event: LayoutChangeEvent): void => { setNavigationHeaderLayout(event.nativeEvent.layout) } + const handleScroll = ( + event: NativeSyntheticEvent + ): void => { + if (targetLayout) { + setTargetHiddenProgress( + // calculate balance header's visibility based on the scroll position + clamp( + event.nativeEvent.contentOffset.y / + (targetLayout.y + targetLayout.height), + 0, + 1 + ) + ) + } + } + useEffect(() => { navigation.setOptions({ headerBackground: renderHeaderBackground, - title: ( + title: header && ( )} - + @@ -51,7 +56,10 @@ export const Confirmation = ({ + sx={{ + textAlign: 'center', + marginTop: 20 + }}> You can now start buying, swapping, sending, receiving crypto and collectibles with no added fees diff --git a/packages/core-mobile/app/new/features/onboarding/components/CreatePin.tsx b/packages/core-mobile/app/new/features/onboarding/components/CreatePin.tsx index 76aeb7d11b..4599dd68ec 100644 --- a/packages/core-mobile/app/new/features/onboarding/components/CreatePin.tsx +++ b/packages/core-mobile/app/new/features/onboarding/components/CreatePin.tsx @@ -60,7 +60,12 @@ export const CreatePin = ({ - + - - {!chosenPinEntered && ( + {!chosenPinEntered && ( + - )} - + + )} diff --git a/packages/core-mobile/app/new/features/onboarding/components/EnterRecoveryPhrase.tsx b/packages/core-mobile/app/new/features/onboarding/components/EnterRecoveryPhrase.tsx index 3fdfd9a85f..5dea612dbd 100644 --- a/packages/core-mobile/app/new/features/onboarding/components/EnterRecoveryPhrase.tsx +++ b/packages/core-mobile/app/new/features/onboarding/components/EnterRecoveryPhrase.tsx @@ -85,7 +85,10 @@ export const EnterRecoveryPhrase = ({ size="large" type="primary" onPress={handleNext} - disabled={!mnemonic}> + disabled={ + !mnemonic || + mnemonic.trim().split(/\s+/).length < MINIMUM_MNEMONIC_WORDS + }> Import @@ -93,3 +96,5 @@ export const EnterRecoveryPhrase = ({ ) } + +const MINIMUM_MNEMONIC_WORDS = 12 diff --git a/packages/core-mobile/app/new/features/onboarding/components/RecoveryPhrase.tsx b/packages/core-mobile/app/new/features/onboarding/components/RecoveryPhrase.tsx index e6bf90e099..e9b4335539 100644 --- a/packages/core-mobile/app/new/features/onboarding/components/RecoveryPhrase.tsx +++ b/packages/core-mobile/app/new/features/onboarding/components/RecoveryPhrase.tsx @@ -26,7 +26,7 @@ export const RecoveryPhrase = ({ function handleNext(): void { showAlert({ - title: 'Security Warning', + title: 'Security warning', description: 'For your security, you should not screenshot your recovery phrase. It is best to write it down and store it in a secure location.', buttons: [ diff --git a/packages/core-mobile/app/new/features/onboarding/components/RecoveryPhraseInput.tsx b/packages/core-mobile/app/new/features/onboarding/components/RecoveryPhraseInput.tsx index 55f5232130..5704f68a9f 100644 --- a/packages/core-mobile/app/new/features/onboarding/components/RecoveryPhraseInput.tsx +++ b/packages/core-mobile/app/new/features/onboarding/components/RecoveryPhraseInput.tsx @@ -149,7 +149,8 @@ export default function RecoveryPhraseInput({ sx={{ backgroundColor: '$surfaceSecondary', borderRadius: 12, - padding: 16, + paddingVertical: 12, + paddingHorizontal: 16, minHeight: 150 }}> )} diff --git a/packages/core-mobile/app/new/features/onboarding/components/SetWalletName.tsx b/packages/core-mobile/app/new/features/onboarding/components/SetWalletName.tsx index e00484f2e8..b8e3a35068 100644 --- a/packages/core-mobile/app/new/features/onboarding/components/SetWalletName.tsx +++ b/packages/core-mobile/app/new/features/onboarding/components/SetWalletName.tsx @@ -18,7 +18,11 @@ export const SetWalletName = ({ - + { const { bottom } = useSafeAreaInsets() const { theme } = useTheme() + const [headerLayout, setHeaderLayout] = useState< + LayoutRectangle | undefined + >() + + const handleHeaderLayout = (event: LayoutChangeEvent): void => { + setHeaderLayout(event.nativeEvent.layout) + } + + const scrollViewProps = useFadingHeaderNavigation({ + targetLayout: headerLayout + }) return ( - + Terms and conditions diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(tabs)/portfolio/index.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(tabs)/portfolio/index.tsx index 91c65a64c8..89e14b1f61 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(tabs)/portfolio/index.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(tabs)/portfolio/index.tsx @@ -9,18 +9,12 @@ import { import { Link } from 'expo-router' import BlurredBarsContentLayout from 'common/components/BlurredBarsContentLayout' import { copyToClipboard } from 'common/utils/clipboard' -import { - LayoutChangeEvent, - LayoutRectangle, - NativeScrollEvent, - NativeSyntheticEvent -} from 'react-native' -import { useAnimatedNavigationHeader } from 'common/hooks/useAnimatedNavigationHeader' -import { clamp } from 'react-native-reanimated' +import { LayoutChangeEvent, LayoutRectangle } from 'react-native' +import { useFadingHeaderNavigation } from 'common/hooks/useFadingHeaderNavigation' import { useApplicationContext } from 'contexts/ApplicationContext' const PortfolioHomeScreen = (): JSX.Element => { - const accountName = 'Account 1' + const accountName = 'Very long wallet name blar blar' const formattedBalance = '$7,377.37' const { appHook: { selectedCurrency } @@ -28,38 +22,19 @@ const PortfolioHomeScreen = (): JSX.Element => { const [balanceHeaderLayout, setBalanceHeaderLayout] = useState< LayoutRectangle | undefined >() - const [balanceHeaderHiddenProgress, setBalanceHeaderHiddenProgress] = - useState(0) // from 0 to 1, 0 = fully hidden, 1 = fully shown - const handleCopyToClipboard = (): void => { copyToClipboard('test') } - const handleScroll = ( - event: NativeSyntheticEvent - ): void => { - if (balanceHeaderLayout) { - setBalanceHeaderHiddenProgress( - // calculate balance header's visibility based on the scroll position - clamp( - event.nativeEvent.contentOffset.y / - (balanceHeaderLayout.y + balanceHeaderLayout.height), - 0, - 1 - ) - ) - } - } - const handleBalanceHeaderLayout = (event: LayoutChangeEvent): void => { setBalanceHeaderLayout(event.nativeEvent.layout) } - useAnimatedNavigationHeader({ - visibilityProgress: balanceHeaderHiddenProgress, + const scrollViewProps = useFadingHeaderNavigation({ header: ( - ) + ), + targetLayout: balanceHeaderLayout }) return ( @@ -71,8 +46,7 @@ const PortfolioHomeScreen = (): JSX.Element => { paddingHorizontal: 16, gap: 16 }} - scrollEventThrottle={16} - onScroll={handleScroll}> + {...scrollViewProps}> { How would you like to access your existing wallet? - } - onPress={handleEnterRecoveryPhrase} - /> - - } - onPress={handleCreateMnemonicWallet} + , + onPress: handleEnterRecoveryPhrase + }, + { + title: 'Create a new wallet', + leftIcon: ( + + ), + onPress: handleCreateMnemonicWallet + } + ]} + itemHeight={60} /> @@ -58,31 +60,4 @@ const AccessWalletScreen = (): JSX.Element => { ) } -const ActionButton = ({ - text, - icon, - onPress -}: { - text: string - icon: JSX.Element - onPress: () => void -}): JSX.Element => { - return ( - - - {icon} - {text} - - - ) -} - export default AccessWalletScreen diff --git a/packages/core-mobile/app/new/routes/loginWithPinOrBiometry.tsx b/packages/core-mobile/app/new/routes/loginWithPinOrBiometry.tsx index 37f8ef02dd..23dd6ae14c 100644 --- a/packages/core-mobile/app/new/routes/loginWithPinOrBiometry.tsx +++ b/packages/core-mobile/app/new/routes/loginWithPinOrBiometry.tsx @@ -94,15 +94,6 @@ const LoginWithPinOrBiometry = (): JSX.Element => { } }) - const avatarContainerMarginTop = useSharedValue( - configuration.avatarContainerMarginTop.from - ) - const avatarContainerStyle = useAnimatedStyle(() => { - return { - marginTop: avatarContainerMarginTop.value - } - }) - const buttonContainerPaddingBottom = useSharedValue( configuration.buttonContainerPaddingBottom.from ) @@ -207,9 +198,6 @@ const LoginWithPinOrBiometry = (): JSX.Element => { const buttonContainerPaddingBottomValue = isEnteringPin ? configuration.buttonContainerPaddingBottom.to : configuration.buttonContainerPaddingBottom.from - const avatarContainerMarginTopValue = isEnteringPin - ? configuration.avatarContainerMarginTop.to - : configuration.avatarContainerMarginTop.from pinInputOpacity.value = withTiming(pinInputOpacityValue, { duration: configuration.animationDuration @@ -220,15 +208,7 @@ const LoginWithPinOrBiometry = (): JSX.Element => { duration: configuration.animationDuration } ) - avatarContainerMarginTop.value = withTiming(avatarContainerMarginTopValue, { - duration: configuration.animationDuration - }) - }, [ - isEnteringPin, - pinInputOpacity, - buttonContainerPaddingBottom, - avatarContainerMarginTop - ]) + }, [isEnteringPin, pinInputOpacity, buttonContainerPaddingBottom]) return ( @@ -259,7 +239,7 @@ const LoginWithPinOrBiometry = (): JSX.Element => { )} - + - Sign in with Google + Continue with Google )} {shouldShowApple && ( @@ -147,7 +147,7 @@ export default function Signup(): JSX.Element { disabled={isRegistering} leftIcon="apple" onPress={handleAppleSignin}> - Sign in with Apple + Continue with Apple )} diff --git a/packages/k2-alpine/package.json b/packages/k2-alpine/package.json index 42a532cca1..fe0b3cadfb 100644 --- a/packages/k2-alpine/package.json +++ b/packages/k2-alpine/package.json @@ -35,6 +35,7 @@ }, "peerDependencies": { "@react-native-masked-view/masked-view": "0.3.0", + "expo-blur": "12.9.2", "expo-image": "1.10.6", "expo-linear-gradient": "12.7.2", "react": "18.3.1", diff --git a/packages/k2-alpine/src/assets/icons/arrow_down.svg b/packages/k2-alpine/src/assets/icons/arrow_down.svg new file mode 100644 index 0000000000..00baead390 --- /dev/null +++ b/packages/k2-alpine/src/assets/icons/arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/k2-alpine/src/components/Avatar/Avatar.tsx b/packages/k2-alpine/src/components/Avatar/Avatar.tsx index 5a634af91e..06d209a0d6 100644 --- a/packages/k2-alpine/src/components/Avatar/Avatar.tsx +++ b/packages/k2-alpine/src/components/Avatar/Avatar.tsx @@ -45,7 +45,7 @@ export const Avatar = ({ const surfacePrimaryBlurBgMap = theme.isDark ? { [theme.colors.$surfacePrimary]: - Platform.OS === 'ios' ? '#050506' : '#0a0a0b', + Platform.OS === 'ios' ? '#37373f' : '#373743', [theme.colors.$surfaceSecondary]: Platform.OS === 'ios' ? '#37373f' : '#373743', [theme.colors.$surfaceTertiary]: diff --git a/packages/k2-alpine/src/components/Avatar/AvatarSelector.tsx b/packages/k2-alpine/src/components/Avatar/AvatarSelector.tsx index 1ae5d1ac58..437887aad5 100644 --- a/packages/k2-alpine/src/components/Avatar/AvatarSelector.tsx +++ b/packages/k2-alpine/src/components/Avatar/AvatarSelector.tsx @@ -2,6 +2,7 @@ import { Dimensions, ImageSourcePropType } from 'react-native' import React, { useMemo, useState } from 'react' import Carousel from 'react-native-reanimated-carousel' import { Pressable } from '../Primitives' +import { isScreenSmall } from '../../utils' import { Avatar } from './Avatar' export const AvatarSelector = ({ @@ -22,6 +23,9 @@ export const AvatarSelector = ({ } }, [avatars]) const [pressedIndex, setPressedIndex] = useState() + const avatarWidth = isScreenSmall + ? configuration.avatarWidth.small + : configuration.avatarWidth.large const handlePressIn = (index: number): void => { setPressedIndex(index) @@ -51,13 +55,13 @@ export const AvatarSelector = ({ return ( handlePressIn(index)} onPressOut={() => handlePressOut(index)} onPress={() => handleSelect(index)}> ) } const configuration = { - avatarWidth: 90, + avatarWidth: { + large: 90, + small: 60 + }, spacing: 6 } diff --git a/packages/k2-alpine/src/components/Avatar/HexagonImageView.tsx b/packages/k2-alpine/src/components/Avatar/HexagonImageView.tsx index b9bbe4b17f..81b59939e2 100644 --- a/packages/k2-alpine/src/components/Avatar/HexagonImageView.tsx +++ b/packages/k2-alpine/src/components/Avatar/HexagonImageView.tsx @@ -119,7 +119,7 @@ export const HexagonBorder = ({ height }: { height: number }): JSX.Element => { fill="none" stroke={theme.isDark ? colors.$neutralWhite : 'black'} strokeOpacity={0.1} - strokeWidth="1" + strokeWidth="2" /> ) diff --git a/packages/k2-alpine/src/components/Button/Button.stories.tsx b/packages/k2-alpine/src/components/Button/Button.stories.tsx index b7f11f485d..96e5aaf6dd 100644 --- a/packages/k2-alpine/src/components/Button/Button.stories.tsx +++ b/packages/k2-alpine/src/components/Button/Button.stories.tsx @@ -5,6 +5,7 @@ import { ScrollView, Text, View } from '../Primitives' import Link from '../../utils/Link' import { useTheme } from '../..' import { Button, ButtonSize, ButtonType } from './Button' +import { FilterButton } from './FilterButton' export default { title: 'Button' @@ -103,6 +104,13 @@ export const All = (): JSX.Element => { }}> disabled + + + diff --git a/packages/k2-alpine/src/components/Button/Button.tsx b/packages/k2-alpine/src/components/Button/Button.tsx index 3706a90960..3e30e6a8e8 100644 --- a/packages/k2-alpine/src/components/Button/Button.tsx +++ b/packages/k2-alpine/src/components/Button/Button.tsx @@ -20,6 +20,12 @@ export type ButtonSize = 'small' | 'medium' | 'large' type ButtonIconType = 'check' | 'expandMore' | 'google' | 'apple' +const BUTTON_ICON_TYPES = ['check', 'expandMore', 'google', 'apple'] as const + +const isButtonIconType = (value: unknown): value is ButtonIconType => { + return (BUTTON_ICON_TYPES as readonly string[]).includes(value as string) +} + interface ButtonProps { onPress?: () => void disabled?: boolean @@ -27,8 +33,8 @@ interface ButtonProps { testID?: string type: ButtonType size: ButtonSize - leftIcon?: ButtonIconType - rightIcon?: ButtonIconType + leftIcon?: ButtonIconType | JSX.Element + rightIcon?: ButtonIconType | JSX.Element } export const Button = forwardRef< @@ -104,16 +110,16 @@ export const Button = forwardRef< alignItems: 'center', ...sizeStyles[size] }}> - {leftIcon ? ( - getIcon(leftIcon, { - width: iconWidth, - height: iconWidth, - color: tintColor, - style: { marginRight: 8 } - }) - ) : rightIcon ? ( - - ) : null} + {React.isValidElement(leftIcon) + ? leftIcon + : isButtonIconType(leftIcon) + ? getIcon(leftIcon, { + width: iconWidth, + height: iconWidth, + color: tintColor, + style: { marginRight: 8 } + }) + : null} {children} - {rightIcon ? ( - getIcon(rightIcon, { - width: iconWidth, - height: iconWidth, - color: tintColor, - style: { marginLeft: 8 } - }) - ) : leftIcon ? ( - - ) : null} + {React.isValidElement(rightIcon) + ? rightIcon + : isButtonIconType(rightIcon) + ? getIcon(rightIcon, { + width: iconWidth, + height: iconWidth, + color: tintColor, + style: { marginLeft: 8 } + }) + : null} @@ -189,7 +195,7 @@ const getBackgroundColor = ( darkModeColors.$surfacePrimary ) : overlayColor( - alpha(darkModeColors.$surfaceSecondary, 0.3), + alpha(darkModeColors.$surfacePrimary, 0.3), lightModeColors.$surfacePrimary ) } @@ -206,7 +212,7 @@ const getBackgroundColor = ( } } -const getTintColor = ( +export const getTintColor = ( type: ButtonType, theme: K2AlpineTheme, disabled: boolean | undefined diff --git a/packages/k2-alpine/src/components/Button/FilterButton.tsx b/packages/k2-alpine/src/components/Button/FilterButton.tsx new file mode 100644 index 0000000000..c7eb2f3ccc --- /dev/null +++ b/packages/k2-alpine/src/components/Button/FilterButton.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { ViewStyle } from 'react-native' +import { Icons } from '../../theme/tokens/Icons' +import { useTheme } from '../../hooks' +import { Button, getTintColor } from './Button' + +export const FilterButton = ({ + title, + style, + disabled, + onPress +}: { + title: string + style?: ViewStyle + disabled?: boolean + onPress?: () => void +}): JSX.Element => { + const { theme } = useTheme() + const tintColor = getTintColor('secondary', theme, disabled) + + return ( + + ) +} diff --git a/packages/k2-alpine/src/components/Dropdown/DropdownBackground.tsx b/packages/k2-alpine/src/components/Dropdown/DropdownBackground.tsx new file mode 100644 index 0000000000..386a259a04 --- /dev/null +++ b/packages/k2-alpine/src/components/Dropdown/DropdownBackground.tsx @@ -0,0 +1,35 @@ +import { BlurView } from 'expo-blur' +import React from 'react' +import { Platform } from 'react-native' +import { View } from '../Primitives' +import { useTheme } from '../../hooks' +import { colors } from '../../theme/tokens/colors' + +export const DropdownBackground = ({ + children +}: { + children: React.ReactNode +}): JSX.Element => { + const { theme } = useTheme() + + return Platform.OS === 'ios' ? ( + + {children} + + ) : ( + + {children} + + ) +} diff --git a/packages/k2-alpine/src/components/Dropdown/SimpleDropdown.stories.tsx b/packages/k2-alpine/src/components/Dropdown/SimpleDropdown.stories.tsx index 77cb9754e2..e0bdd49415 100644 --- a/packages/k2-alpine/src/components/Dropdown/SimpleDropdown.stories.tsx +++ b/packages/k2-alpine/src/components/Dropdown/SimpleDropdown.stories.tsx @@ -1,8 +1,11 @@ -import React, { useState } from 'react' +import React, { useRef, useState } from 'react' +import { TouchableOpacity } from 'react-native' +import { Rect } from 'react-native-popover-view' import { ScrollView, Text, View } from '../Primitives' import { Button } from '../Button/Button' import { showAlert } from '../Alert/Alert' import { IndexPath, SimpleDropdown } from './SimpleDropdown' +import { usePopoverAnchor } from './usePopoverAnchor' export default { title: 'Dropdown' @@ -20,6 +23,8 @@ export const All = (): JSX.Element => { + + ) @@ -178,3 +183,89 @@ const MultipleSectionMultipleSelectionDropdown = (): JSX.Element => { ) } + +const LeftAlignedDropdown = (): JSX.Element => { + const sections = [ + ['All networks', 'Avalanche C-Chain', 'Bitcoin network', 'Ethereum'] + ] + const [selectedRow, setSelectedRow] = useState({ + section: 0, + row: 0 + }) + const sourceRef = useRef(null) + + const { anchorRect, isPopoverVisible, onShowPopover, onHidePopover } = + usePopoverAnchor(sourceRef) + + return ( + + Left aligned + + setSelectedRow(indexPath)} + onRequestClose={onHidePopover} + /> + + ) +} + +const RightAlignedDropdown = (): JSX.Element => { + const sections = [ + ['All networks', 'Avalanche C-Chain', 'Bitcoin network', 'Ethereum'] + ] + const [selectedRow, setSelectedRow] = useState({ + section: 0, + row: 0 + }) + const sourceRef = useRef(null) + + const { anchorRect, isPopoverVisible, onShowPopover, onHidePopover } = + usePopoverAnchor(sourceRef) + + return ( + + Right aligned + + setSelectedRow(indexPath)} + onRequestClose={onHidePopover} + /> + + ) +} diff --git a/packages/k2-alpine/src/components/Dropdown/SimpleDropdown.tsx b/packages/k2-alpine/src/components/Dropdown/SimpleDropdown.tsx index a64686d297..8f1cb5c2c4 100644 --- a/packages/k2-alpine/src/components/Dropdown/SimpleDropdown.tsx +++ b/packages/k2-alpine/src/components/Dropdown/SimpleDropdown.tsx @@ -1,10 +1,11 @@ import React, { useRef } from 'react' -import Popover from 'react-native-popover-view' -import { BlurView } from 'expo-blur' +import Popover, { Rect } from 'react-native-popover-view' +import { Platform } from 'react-native' import { Text, View, TouchableOpacity } from '../Primitives' import { useTheme } from '../../hooks' import { Separator } from '../Separator/Separator' import { Icons } from '../../theme/tokens/Icons' +import { DropdownBackground } from './DropdownBackground' export const SimpleDropdown = ({ from, @@ -13,15 +14,19 @@ export const SimpleDropdown = ({ offset = 0, allowsMultipleSelection = false, onSelectRow, - onDeselectRow + onDeselectRow, + onRequestClose, + isVisible }: { - from: React.ReactNode + from: React.ReactNode | Rect sections: T[][] selectedRows: IndexPath[] allowsMultipleSelection?: boolean offset?: number onSelectRow: (indexPath: IndexPath) => void onDeselectRow?: (indexPath: IndexPath) => void + onRequestClose?: () => void + isVisible?: boolean }): JSX.Element => { const { theme } = useTheme() const popoverRef = useRef() @@ -48,28 +53,28 @@ export const SimpleDropdown = ({ ) } + const groupSeparatorHeight = Platform.OS === 'ios' ? 6 : 1 + const backgroundBorderRadius = Platform.OS === 'ios' ? 10 : 4 + return ( - + {sections.map((section, sectionIndex) => { return ( @@ -107,14 +112,15 @@ export const SimpleDropdown = ({ )} - {rowIndex !== section.length - 1 && } + {rowIndex !== section.length - 1 && + Platform.OS === 'ios' && } ) })} {sectionIndex !== sections.length - 1 && ( @@ -122,7 +128,7 @@ export const SimpleDropdown = ({ ) })} - + ) } diff --git a/packages/k2-alpine/src/components/Dropdown/usePopoverAnchor.ts b/packages/k2-alpine/src/components/Dropdown/usePopoverAnchor.ts new file mode 100644 index 0000000000..9548c2cb24 --- /dev/null +++ b/packages/k2-alpine/src/components/Dropdown/usePopoverAnchor.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react' +import { NativeMethods } from 'react-native' +import { Rect } from 'react-native-popover-view' + +export const usePopoverAnchor = ( + sourceRef: React.RefObject +): { + anchorRect: Rect | undefined + isPopoverVisible: boolean + onShowPopover: () => void + onHidePopover: () => void +} => { + const [isPopoverVisible, setIsPopoverVisible] = useState(false) + const [anchorRect, setAnchorRect] = useState() + + const showPopover = (): void => { + if (sourceRef.current) { + // eslint-disable-next-line max-params + sourceRef.current.measureInWindow((x, y, width, height) => { + setAnchorRect(new Rect(x, y, width, height)) + }) + } + } + + const hidePopover = (): void => { + setAnchorRect(undefined) + } + + useEffect(() => { + if (anchorRect) { + setIsPopoverVisible(true) + } else { + setIsPopoverVisible(false) + } + }, [anchorRect]) + + return { + anchorRect, + isPopoverVisible, + onShowPopover: showPopover, + onHidePopover: hidePopover + } +} diff --git a/packages/k2-alpine/src/components/GroupList/GroupList.stories.tsx b/packages/k2-alpine/src/components/GroupList/GroupList.stories.tsx index 7f1f88d96b..2a41968473 100644 --- a/packages/k2-alpine/src/components/GroupList/GroupList.stories.tsx +++ b/packages/k2-alpine/src/components/GroupList/GroupList.stories.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { Switch } from 'react-native' import { ScrollView, View } from '../Primitives' -import { showAlert, useTheme } from '../..' +import { Icons, showAlert, useTheme } from '../..' import { GroupList } from './GroupList' export default { @@ -52,6 +52,25 @@ export const All = (): JSX.Element => { } ]} /> + , + onPress: () => { + showAlert({ title: 'Pressed', buttons: [{ text: 'OK' }] }) + } + }, + { + title: 'Create a new wallet', + leftIcon: , + onPress: () => { + showAlert({ title: 'Pressed', buttons: [{ text: 'OK' }] }) + } + } + ]} + itemHeight={60} + /> ) diff --git a/packages/k2-alpine/src/components/GroupList/GroupList.tsx b/packages/k2-alpine/src/components/GroupList/GroupList.tsx index 23e713784b..c0513fa35c 100644 --- a/packages/k2-alpine/src/components/GroupList/GroupList.tsx +++ b/packages/k2-alpine/src/components/GroupList/GroupList.tsx @@ -1,55 +1,84 @@ -import React from 'react' -import { View, Text, TouchableHighlight } from '../Primitives' +import React, { useState } from 'react' +import { LayoutChangeEvent } from 'react-native' +import { View, Text, TouchableOpacity } from '../Primitives' import { Separator } from '../Separator/Separator' import { Icons } from '../../theme/tokens/Icons' import { useTheme } from '../../hooks' -export const GroupList = ({ data }: { data: GroupListItem[] }): JSX.Element => { +export const GroupList = ({ + data, + itemHeight +}: { + data: GroupListItem[] + itemHeight?: number +}): JSX.Element => { const { theme } = useTheme() + const [textMarginLeft, setTextMarginLeft] = useState(0) + + const handleLayout = (event: LayoutChangeEvent): void => { + setTextMarginLeft(event.nativeEvent.layout.x) + } return ( - - {data.map(({ title, value, accessory, onPress }, index) => ( + + {data.map(({ leftIcon, title, value, accessory, onPress }, index) => ( - + - - {title} - + {leftIcon && {leftIcon}} - {value !== undefined && ( - - {value} - - )} - {accessory !== undefined && accessory} - {accessory === undefined && onPress !== undefined && ( - - )} + justifyContent: 'space-between', + marginLeft: 15 + }} + onLayout={handleLayout}> + + {title} + + + {value !== undefined && ( + + {value} + + )} + {accessory !== undefined && accessory} + {accessory === undefined && onPress !== undefined && ( + + )} + - - {index < data.length - 1 && } + + {index < data.length - 1 && ( + + )} ))} @@ -60,5 +89,6 @@ export type GroupListItem = { title: string value?: string onPress?: () => void + leftIcon?: JSX.Element accessory?: JSX.Element } diff --git a/packages/k2-alpine/src/components/Header/BalanceHeader.tsx b/packages/k2-alpine/src/components/Header/BalanceHeader.tsx index 5464c34b1c..123621b2d6 100644 --- a/packages/k2-alpine/src/components/Header/BalanceHeader.tsx +++ b/packages/k2-alpine/src/components/Header/BalanceHeader.tsx @@ -29,7 +29,8 @@ export const BalanceHeader = ({ + sx={{ color: '$textSecondary', lineHeight: 38, marginRight: 100 }} + numberOfLines={1}> {accountName} diff --git a/packages/k2-alpine/src/components/Header/NavigationTitleHeader.tsx b/packages/k2-alpine/src/components/Header/NavigationTitleHeader.tsx index 32298d2a2e..dcf0b051a2 100644 --- a/packages/k2-alpine/src/components/Header/NavigationTitleHeader.tsx +++ b/packages/k2-alpine/src/components/Header/NavigationTitleHeader.tsx @@ -6,11 +6,20 @@ export const NavigationTitleHeader = memo( return ( + numberOfLines={1} + sx={{ + fontSize: 17, + lineHeight: 20, + fontFamily: 'Inter-SemiBold', + maxWidth: 150 + }}> {title} {subtitle && ( - + {subtitle} )} diff --git a/packages/k2-alpine/src/components/TextInput/TextInput.tsx b/packages/k2-alpine/src/components/TextInput/TextInput.tsx index 1efbec2294..0b935620bc 100644 --- a/packages/k2-alpine/src/components/TextInput/TextInput.tsx +++ b/packages/k2-alpine/src/components/TextInput/TextInput.tsx @@ -11,6 +11,7 @@ interface TextInputProps { sx?: SxProp rightIcon?: React.ReactNode leftIcon?: React.ReactNode + maxLength?: number } export const TextInput = ({ @@ -20,7 +21,8 @@ export const TextInput = ({ placeholder, onChangeText, rightIcon, - leftIcon + leftIcon, + maxLength }: TextInputProps): React.JSX.Element => { const { theme: { colors } @@ -29,7 +31,7 @@ export const TextInput = ({ return ( diff --git a/packages/k2-alpine/src/theme/tokens/Icons.ts b/packages/k2-alpine/src/theme/tokens/Icons.ts index fd374eaf4b..f752d86236 100644 --- a/packages/k2-alpine/src/theme/tokens/Icons.ts +++ b/packages/k2-alpine/src/theme/tokens/Icons.ts @@ -33,6 +33,7 @@ import IconTrendingArrowDown from '../../assets/icons/trending_arrow_down.svg' import IconQRCode2 from '../../assets/icons/qr_code_2.svg' import IconNotifications from '../../assets/icons/notifications.svg' import IconAlertError from '../../assets/icons/alert_error.svg' +import IconArrowDown from '../../assets/icons/arrow_down.svg' export const Icons = { Action: { @@ -74,7 +75,8 @@ export const Icons = { Psychiatry: IconPsychiatry, Error: IconError, TrendingArrowUp: IconTrendingArrowUp, - TrendingArrowDown: IconTrendingArrowDown + TrendingArrowDown: IconTrendingArrowDown, + ArrowDown: IconArrowDown }, RecoveryMethod: { Passkey: IconPasskey, diff --git a/packages/k2-alpine/src/theme/tokens/colors.ts b/packages/k2-alpine/src/theme/tokens/colors.ts index ddce9fc570..3751cd3158 100644 --- a/packages/k2-alpine/src/theme/tokens/colors.ts +++ b/packages/k2-alpine/src/theme/tokens/colors.ts @@ -44,8 +44,8 @@ export const darkModeColors = { $textSuccess: colors.$accentSuccessD, // surface - $surfacePrimary: colors.$neutral950, - $surfaceSecondary: colors.$neutral850, + $surfacePrimary: colors.$neutral850, + $surfaceSecondary: alpha(colors.$neutralWhite, 0.1), $surfaceTertiary: colors.$neutral900, // border diff --git a/packages/k2-alpine/src/utils/index.ts b/packages/k2-alpine/src/utils/index.ts index 67b2602ea5..6d56fbcf6c 100644 --- a/packages/k2-alpine/src/utils/index.ts +++ b/packages/k2-alpine/src/utils/index.ts @@ -1 +1,2 @@ export * from './colors' +export * from './screens' diff --git a/packages/k2-alpine/src/utils/screens.ts b/packages/k2-alpine/src/utils/screens.ts new file mode 100644 index 0000000000..f4659ef808 --- /dev/null +++ b/packages/k2-alpine/src/utils/screens.ts @@ -0,0 +1,5 @@ +import { Dimensions } from 'react-native' + +const SCREEN_HEIGHT = Dimensions.get('window').height + +export const isScreenSmall = SCREEN_HEIGHT <= 670 diff --git a/yarn.lock b/yarn.lock index 2b34f31f2c..7da1d02cbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -559,6 +559,7 @@ __metadata: typescript: 5.6.3 peerDependencies: "@react-native-masked-view/masked-view": 0.3.0 + expo-blur: 12.9.2 expo-image: 1.10.6 expo-linear-gradient: 12.7.2 react: 18.3.1 @@ -27107,7 +27108,7 @@ react-native-webview@ava-labs/react-native-webview: peerDependencies: react: "*" react-native: "*" - checksum: 77324747a8b5df0a5558bb99a9a0804a5575d328e84f480e462c56417af97213f38a9930e4582fb749bec374dc4d1a8910a45b006e77af9b14a8e64057b932bf + checksum: 869028a5bb7a4a8a125d274753b703c2b3579b5efc7bb82db136a186fd88a11c5a4696505ebcd976d20fcdd41111abe229ae0e19c27c50bb30fe4f52bb6383bc languageName: node linkType: hard