From a6edc2dcf20c7bab6c47554d36a2d5b52bde86b9 Mon Sep 17 00:00:00 2001 From: Fara Woolf Date: Wed, 31 Jan 2024 13:44:30 -0600 Subject: [PATCH] refactor: interactive item --- .../components/account/account-addresses.tsx | 28 ++++ .../account/account-list-item-layout.tsx | 95 ----------- .../account/account-list-item.layout.tsx | 58 +++++++ .../brc20-token-asset-list.tsx | 1 - .../brc20-token-asset-item.layout.tsx | 43 +++-- .../crypto-asset-list-item.tsx | 1 - .../choose-crypto-asset/crypto-asset-list.tsx | 1 - .../fungible-token-asset-item.tsx | 8 +- .../crypto-currency-asset-item.layout.tsx | 73 ++++----- ...tacks-fungible-token-asset-item.layout.tsx | 61 ++++---- .../transaction-item.layout.tsx | 46 +++--- .../components/switch-account-list-item.tsx | 19 ++- .../choose-account/components/accounts.tsx | 4 +- .../pages/receive/components/receive-item.tsx | 56 +++---- .../account-list-item.tsx | 5 +- .../components/swap-asset-item.tsx | 29 ++-- .../components/swap-asset-list.tsx | 2 - .../dowpdown-menu/dropdown-menu.tsx | 20 +-- .../item/item-interactive.stories.tsx | 75 +++++++++ .../ui/components/item/item-interactive.tsx | 99 ++++++++++++ .../item/item-with-buttons.layout.tsx | 45 ++++++ src/app/ui/components/item/item.layout.tsx | 81 ++++++++++ .../components/items/item-default.layout.tsx | 59 ------- .../items/item-with-buttons.layout.tsx | 45 ------ src/app/ui/components/items/item.stories.tsx | 147 ------------------ src/app/ui/components/items/item.tsx | 137 ---------------- src/app/ui/components/select/select.tsx | 16 +- src/app/ui/components/typography/caption.tsx | 20 ++- src/app/ui/components/typography/title.tsx | 26 ++-- 29 files changed, 591 insertions(+), 709 deletions(-) create mode 100644 src/app/components/account/account-addresses.tsx delete mode 100644 src/app/components/account/account-list-item-layout.tsx create mode 100644 src/app/components/account/account-list-item.layout.tsx create mode 100644 src/app/ui/components/item/item-interactive.stories.tsx create mode 100644 src/app/ui/components/item/item-interactive.tsx create mode 100644 src/app/ui/components/item/item-with-buttons.layout.tsx create mode 100644 src/app/ui/components/item/item.layout.tsx delete mode 100644 src/app/ui/components/items/item-default.layout.tsx delete mode 100644 src/app/ui/components/items/item-with-buttons.layout.tsx delete mode 100644 src/app/ui/components/items/item.stories.tsx delete mode 100644 src/app/ui/components/items/item.tsx diff --git a/src/app/components/account/account-addresses.tsx b/src/app/components/account/account-addresses.tsx new file mode 100644 index 00000000000..9a2fda9ec52 --- /dev/null +++ b/src/app/components/account/account-addresses.tsx @@ -0,0 +1,28 @@ +import { HStack } from 'leather-styles/jsx'; + +import { useViewportMinWidth } from '@app/common/hooks/use-media-query'; +import { BulletSeparator } from '@app/ui/components/bullet-separator/bullet-separator'; +import { truncateMiddle } from '@app/ui/utils/truncate-middle'; + +import { StacksAccountLoader } from '../loaders/stacks-account-loader'; +import { BitcoinNativeSegwitAccountLoader } from './bitcoin-account-loader'; + +interface AccountAddressesProps { + index: number; +} +export function AcccountAddresses({ index }: AccountAddressesProps) { + const isBreakpointSm = useViewportMinWidth('sm'); + + return ( + + + + {account => <>{truncateMiddle(account.address, isBreakpointSm ? 4 : 3)}} + + + {signer => <>{truncateMiddle(signer.address, 5)}} + + + + ); +} diff --git a/src/app/components/account/account-list-item-layout.tsx b/src/app/components/account/account-list-item-layout.tsx deleted file mode 100644 index 82cc25d2a59..00000000000 --- a/src/app/components/account/account-list-item-layout.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { ComponentProps, ReactNode } from 'react'; - -import { SettingsSelectors } from '@tests/selectors/settings.selectors'; -import { HStack } from 'leather-styles/jsx'; - -import { useViewportMinWidth } from '@app/common/hooks/use-media-query'; -import { BulletSeparator } from '@app/ui/components/bullet-separator/bullet-separator'; -import { CheckmarkIcon } from '@app/ui/components/icons/checkmark-icon'; -import { Item } from '@app/ui/components/items/item'; -import { ItemDefaultLayout } from '@app/ui/components/items/item-default.layout'; -import { Spinner } from '@app/ui/components/spinner'; -import { truncateMiddle } from '@app/ui/utils/truncate-middle'; - -import { StacksAccountLoader } from '../loaders/stacks-account-loader'; -import { BitcoinNativeSegwitAccountLoader } from './bitcoin-account-loader'; - -interface AccountListItemLayoutProps extends ComponentProps<'div'> { - accountName: ReactNode; - avatar: ReactNode; - balanceLabel: ReactNode; - index: number; - isLoading: boolean; - isPressable?: boolean; - isSelected: boolean; - onSelectAccount(): void; -} -export function AccountListItemLayout(props: AccountListItemLayoutProps) { - const { - accountName, - avatar, - balanceLabel, - index, - isLoading, - isPressable, - isSelected, - onSelectAccount, - } = props; - - const isBreakpointSm = useViewportMinWidth('sm'); - - return ( - - - - {accountName} - {isSelected && } - - } - contentLeftBottom={ - - - - {account => ( - - {truncateMiddle(account.address, isBreakpointSm ? 4 : 3)} - - )} - - - {signer => ( - - {truncateMiddle(signer.address, 5)} - - )} - - - - } - contentRightTop={ - isLoading ? ( - - ) : ( - {balanceLabel} - ) - } - /> - - - ); -} diff --git a/src/app/components/account/account-list-item.layout.tsx b/src/app/components/account/account-list-item.layout.tsx new file mode 100644 index 00000000000..f353437b7ec --- /dev/null +++ b/src/app/components/account/account-list-item.layout.tsx @@ -0,0 +1,58 @@ +import { ReactNode } from 'react'; + +import { SettingsSelectors } from '@tests/selectors/settings.selectors'; + +import { ItemInteractive } from '@app/ui/components/item/item-interactive'; +import { ItemLayout } from '@app/ui/components/item/item.layout'; +import { Spinner } from '@app/ui/components/spinner'; + +interface AccountListItemLayoutProps { + accountAddresses: ReactNode; + accountName: ReactNode; + avatar: ReactNode; + balanceLabel: ReactNode; + index: number; + isLoading: boolean; + isSelected: boolean; + onSelectAccount(): void; +} +export function AccountListItemLayout(props: AccountListItemLayoutProps) { + const { + accountAddresses, + accountName, + avatar, + balanceLabel, + index, + isLoading, + isSelected, + onSelectAccount, + } = props; + + return ( + + + ) : ( + balanceLabel + ) + } + captionLeft={accountAddresses} + /> + + ); +} diff --git a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx index 8652feb9670..80e06aa8328 100644 --- a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx +++ b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx @@ -34,7 +34,6 @@ export function Brc20TokenAssetList(props: { brc20Tokens?: Brc20Token[] }) { key={token.tick} displayNotEnoughBalance={!hasPositiveBtcBalanceForFees} token={token} - isPressable={hasPositiveBtcBalanceForFees} onClick={hasPositiveBtcBalanceForFees ? () => navigateToBrc20SendForm(token) : noop} /> ))} diff --git a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/components/brc20-token-asset-item.layout.tsx b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/components/brc20-token-asset-item.layout.tsx index 750b729abc4..fe66492d136 100644 --- a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/components/brc20-token-asset-item.layout.tsx +++ b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/components/brc20-token-asset-item.layout.tsx @@ -1,20 +1,20 @@ +import { styled } from 'leather-styles/jsx'; + import { createMoney } from '@shared/models/money.model'; import { formatBalance } from '@app/common/format-balance'; import { Brc20Token } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query'; import { Brc20TokenIcon } from '@app/ui/components/icons/brc20-token-icon'; -import { Item } from '@app/ui/components/items/item'; -import { ItemDefaultLayout } from '@app/ui/components/items/item-default.layout'; +import { ItemInteractive } from '@app/ui/components/item/item-interactive'; +import { ItemLayout } from '@app/ui/components/item/item.layout'; import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip'; interface Brc20TokenAssetItemLayoutProps { token: Brc20Token; - isPressable?: boolean; onClick?(): void; displayNotEnoughBalance?: boolean; } export function Brc20TokenAssetItemLayout({ - isPressable, onClick, displayNotEnoughBalance, token, @@ -29,24 +29,23 @@ export function Brc20TokenAssetItemLayout({ label={'Not enough BTC in balance'} side="top" > - - - } - contentLeftTop={{token.tick}} - contentLeftBottom={{'BRC-20'}} - contentRightTop={ - - {formattedBalance.value} - - } - /> - - + + } + titleLeft={token.tick} + captionLeft={'BRC-20'} + titleRight={ + + + {formattedBalance.value} + + + } + /> + ); } diff --git a/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list-item.tsx b/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list-item.tsx index 81eb70a1ecc..4e790a4e35f 100644 --- a/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list-item.tsx +++ b/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list-item.tsx @@ -18,7 +18,6 @@ export function CryptoAssetListItem(props: CryptoAssetListItemProps) { } - isPressable onClick={onClick} /> ); diff --git a/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list.tsx b/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list.tsx index d40c64ede1f..0605e67c3bf 100644 --- a/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list.tsx +++ b/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list.tsx @@ -29,7 +29,6 @@ export function CryptoAssetList({ cryptoAssetBalances, onItemClick }: CryptoAsse assetBalance={balance} icon={} onClick={() => onItemClick(balance)} - isPressable /> )} diff --git a/src/app/components/crypto-assets/choose-crypto-asset/fungible-token-asset-item.tsx b/src/app/components/crypto-assets/choose-crypto-asset/fungible-token-asset-item.tsx index 8d7389ea14f..7be8034c33e 100644 --- a/src/app/components/crypto-assets/choose-crypto-asset/fungible-token-asset-item.tsx +++ b/src/app/components/crypto-assets/choose-crypto-asset/fungible-token-asset-item.tsx @@ -13,13 +13,7 @@ export function FungibleTokenAssetItem({ assetBalance, onClick }: FungibleTokenA switch (blockchain) { case 'stacks': - return ( - - ); + return ; default: return null; } diff --git a/src/app/components/crypto-assets/crypto-currency-asset/crypto-currency-asset-item.layout.tsx b/src/app/components/crypto-assets/crypto-currency-asset/crypto-currency-asset-item.layout.tsx index d5ff281458e..f603857fd12 100644 --- a/src/app/components/crypto-assets/crypto-currency-asset/crypto-currency-asset-item.layout.tsx +++ b/src/app/components/crypto-assets/crypto-currency-asset/crypto-currency-asset-item.layout.tsx @@ -1,9 +1,11 @@ import { ReactNode } from 'react'; +import { styled } from 'leather-styles/jsx'; + import { AllCryptoCurrencyAssetBalances } from '@shared/models/crypto-asset-balance.model'; -import { Item } from '@app/ui/components/items/item'; -import { ItemDefaultLayout } from '@app/ui/components/items/item-default.layout'; +import { ItemInteractive } from '@app/ui/components/item/item-interactive'; +import { ItemLayout } from '@app/ui/components/item/item.layout'; import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip'; import { parseCryptoCurrencyAssetBalance } from './crypto-currency-asset.utils'; @@ -14,7 +16,6 @@ interface CryptoCurrencyAssetItemLayoutProps { address?: string; assetBalance: AllCryptoCurrencyAssetBalances; icon: React.ReactNode; - isPressable?: boolean; onClick?(): void; rightElement?: React.ReactNode; usdBalance?: string; @@ -25,7 +26,6 @@ export function CryptoCurrencyAssetItemLayout({ address = '', assetBalance, icon, - isPressable, onClick, rightElement, usdBalance, @@ -34,40 +34,35 @@ export function CryptoCurrencyAssetItemLayout({ parseCryptoCurrencyAssetBalance(assetBalance); return ( - - - {title}} - contentLeftBottom={{balance.symbol}} - contentRightTop={ - rightElement ? ( - rightElement - ) : ( - - - {formattedBalance.value} {additionalBalanceInfo} - - - ) - } - contentRightBottom={ - !rightElement && ( - <> - {balance.amount.toNumber() > 0 && address ? ( - {usdBalance} - ) : null} - {additionalUsdBalanceInfo} - - ) - } - /> - - + + + + {formattedBalance.value} {additionalBalanceInfo} + + + ) + } + captionRight={ + !rightElement && ( + <> + {balance.amount.toNumber() > 0 && address ? usdBalance : null} + {additionalUsdBalanceInfo} + + ) + } + /> + ); } diff --git a/src/app/components/crypto-assets/stacks/fungible-token-asset/stacks-fungible-token-asset-item.layout.tsx b/src/app/components/crypto-assets/stacks/fungible-token-asset/stacks-fungible-token-asset-item.layout.tsx index d47bb580151..79b0f6fc747 100644 --- a/src/app/components/crypto-assets/stacks/fungible-token-asset/stacks-fungible-token-asset-item.layout.tsx +++ b/src/app/components/crypto-assets/stacks/fungible-token-asset/stacks-fungible-token-asset-item.layout.tsx @@ -1,52 +1,51 @@ +import { styled } from 'leather-styles/jsx'; + import { StacksFungibleTokenAssetBalance } from '@shared/models/crypto-asset-balance.model'; import { StacksAssetAvatar } from '@app/components/crypto-assets/stacks/components/stacks-asset-avatar'; -import { Item } from '@app/ui/components/items/item'; -import { ItemDefaultLayout } from '@app/ui/components/items/item-default.layout'; +import { ItemInteractive } from '@app/ui/components/item/item-interactive'; +import { ItemLayout } from '@app/ui/components/item/item.layout'; import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip'; import { parseStacksFungibleTokenAssetBalance } from './fungible-token-asset.utils'; interface StacksFungibleTokenAssetItemLayoutProps { assetBalance: StacksFungibleTokenAssetBalance; - isPressable?: boolean; onClick?(): void; } export function StacksFungibleTokenAssetItemLayout({ assetBalance, - isPressable, onClick, }: StacksFungibleTokenAssetItemLayoutProps) { const { amount, avatar, caption, dataTestId, formattedBalance, imageCanonicalUri, title } = parseStacksFungibleTokenAssetBalance(assetBalance); return ( - - - - {title[0]} - - } - contentLeftTop={{title}} - contentLeftBottom={{caption}} - contentRightTop={ - - {formattedBalance.value} - - } - /> - - + + + {title[0]} + + } + titleLeft={title} + captionLeft={caption} + titleRight={ + + + {formattedBalance.value} + + + } + /> + ); } diff --git a/src/app/components/transaction-item/transaction-item.layout.tsx b/src/app/components/transaction-item/transaction-item.layout.tsx index 4248f843e04..e778cf9591d 100644 --- a/src/app/components/transaction-item/transaction-item.layout.tsx +++ b/src/app/components/transaction-item/transaction-item.layout.tsx @@ -1,7 +1,9 @@ import { ReactNode } from 'react'; -import { Item } from '@app/ui/components/items/item'; -import { ItemDefaultLayout } from '@app/ui/components/items/item-default.layout'; +import { styled } from 'leather-styles/jsx'; + +import { ItemInteractive } from '@app/ui/components/item/item-interactive'; +import { ItemLayout } from '@app/ui/components/item/item.layout'; interface TransactionItemLayoutProps { openTxLink(): void; @@ -23,21 +25,29 @@ export function TransactionItemLayout({ txValue, }: TransactionItemLayoutProps) { return ( - - - {txTitle}} - contentLeftBottom={ - <> - {txCaption} - {txStatus && txStatus} - - } - contentRightTop={rightElement ? rightElement : {txValue}} - /> - - + // TODO: Revisit if needed styles position="relative" zIndex={99} + + + + {txCaption} + + {txStatus && txStatus} + + } + titleRight={ + rightElement ? ( + rightElement + ) : ( + + {txValue} + + ) + } + /> + ); } diff --git a/src/app/features/switch-account-drawer/components/switch-account-list-item.tsx b/src/app/features/switch-account-drawer/components/switch-account-list-item.tsx index d5e77122983..384fb50567c 100644 --- a/src/app/features/switch-account-drawer/components/switch-account-list-item.tsx +++ b/src/app/features/switch-account-drawer/components/switch-account-list-item.tsx @@ -4,12 +4,13 @@ import { useAccountDisplayName } from '@app/common/hooks/account/use-account-nam import { useSwitchAccount } from '@app/common/hooks/account/use-switch-account'; import { useLoading } from '@app/common/hooks/use-loading'; import { AccountTotalBalance } from '@app/components/account-total-balance'; -import { AccountListItemLayout } from '@app/components/account/account-list-item-layout'; +import { AcccountAddresses } from '@app/components/account/account-addresses'; +import { AccountListItemLayout } from '@app/components/account/account-list-item.layout'; +import { AccountNameLayout } from '@app/components/account/account-name'; import { useNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useStacksAccounts } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { AccountAvatarItem } from '../../../components/account/account-avatar-item'; -import { AccountNameLayout } from '../../../components/account/account-name'; interface SwitchAccountListItemProps { handleClose(): void; @@ -19,15 +20,15 @@ interface SwitchAccountListItemProps { export const SwitchAccountListItem = memo( ({ handleClose, currentAccountIndex, index }: SwitchAccountListItemProps) => { const stacksAccounts = useStacksAccounts(); - const stacksAddress = stacksAccounts[index]?.address || ''; + const stxAddress = stacksAccounts[index]?.address || ''; const bitcoinSigner = useNativeSegwitSigner(index); - const bitcoinAddress = bitcoinSigner?.(0).address || ''; + const btcAddress = bitcoinSigner?.(0).address || ''; const { isLoading, setIsLoading, setIsIdle } = useLoading( - 'SWITCH_ACCOUNTS' + stacksAddress || bitcoinAddress + 'SWITCH_ACCOUNTS' + stxAddress || btcAddress ); const { handleSwitchAccount } = useSwitchAccount(handleClose); - const name = useAccountDisplayName({ address: stacksAddress, index }); + const name = useAccountDisplayName({ address: stxAddress, index }); const handleClick = async () => { setIsLoading(); @@ -39,6 +40,7 @@ export const SwitchAccountListItem = memo( return ( } accountName={{name}} avatar={ } - balanceLabel={ - - } + balanceLabel={} index={index} isLoading={isLoading} - isPressable isSelected={currentAccountIndex === index} onSelectAccount={handleClick} /> diff --git a/src/app/pages/choose-account/components/accounts.tsx b/src/app/pages/choose-account/components/accounts.tsx index f22b61e946d..793396e2589 100644 --- a/src/app/pages/choose-account/components/accounts.tsx +++ b/src/app/pages/choose-account/components/accounts.tsx @@ -12,8 +12,9 @@ import { useCreateAccount } from '@app/common/hooks/account/use-create-account'; import { useWalletType } from '@app/common/use-wallet-type'; import { slugify } from '@app/common/utils'; import { AccountTotalBalance } from '@app/components/account-total-balance'; +import { AcccountAddresses } from '@app/components/account/account-addresses'; import { AccountAvatar } from '@app/components/account/account-avatar'; -import { AccountListItemLayout } from '@app/components/account/account-list-item-layout'; +import { AccountListItemLayout } from '@app/components/account/account-list-item.layout'; import { usePressable } from '@app/components/item-hover'; import { useNativeSegwitAccountIndexAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useStacksAccounts } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; @@ -45,6 +46,7 @@ const ChooseAccountItem = memo( // Padding required on outer element to prevent jumpy virtualized list } accountName={ }> {name} diff --git a/src/app/pages/receive/components/receive-item.tsx b/src/app/pages/receive/components/receive-item.tsx index a6d7e11f078..d4b79796505 100644 --- a/src/app/pages/receive/components/receive-item.tsx +++ b/src/app/pages/receive/components/receive-item.tsx @@ -1,10 +1,8 @@ -import { HStack } from 'leather-styles/jsx'; - import { Button } from '@app/ui/components/button/button'; import { CopyIcon } from '@app/ui/components/icons/copy-icon'; import { QrCodeIcon } from '@app/ui/components/icons/qr-code-icon'; -import { Item } from '@app/ui/components/items/item'; -import { ItemWithButtonsLayout } from '@app/ui/components/items/item-with-buttons.layout'; +import { ItemInteractive } from '@app/ui/components/item/item-interactive'; +import { ItemWithButtonsLayout } from '@app/ui/components/item/item-with-buttons.layout'; import { truncateMiddle } from '@app/ui/utils/truncate-middle'; interface ReceiveItemProps { @@ -25,33 +23,29 @@ export function ReceiveItem({ }: ReceiveItemProps) { if (!address) return null; return ( - - - {title}} - contentLeftBottom={ - {truncateMiddle(address, 6)} - } - contentRightTop={ - - + {onClickQrCode && ( + - {onClickQrCode && ( - - )} - - } - /> - - + )} + + } + /> + ); } diff --git a/src/app/pages/send/send-crypto-asset-form/components/recipient-accounts-drawer/account-list-item.tsx b/src/app/pages/send/send-crypto-asset-form/components/recipient-accounts-drawer/account-list-item.tsx index c55652b6522..d59114ed9d0 100644 --- a/src/app/pages/send/send-crypto-asset-form/components/recipient-accounts-drawer/account-list-item.tsx +++ b/src/app/pages/send/send-crypto-asset-form/components/recipient-accounts-drawer/account-list-item.tsx @@ -6,8 +6,9 @@ import { BitcoinSendFormValues, StacksSendFormValues } from '@shared/models/form import { useAccountDisplayName } from '@app/common/hooks/account/use-account-names'; import { AccountTotalBalance } from '@app/components/account-total-balance'; +import { AcccountAddresses } from '@app/components/account/account-addresses'; import { AccountAvatarItem } from '@app/components/account/account-avatar-item'; -import { AccountListItemLayout } from '@app/components/account/account-list-item-layout'; +import { AccountListItemLayout } from '@app/components/account/account-list-item.layout'; import { AccountNameLayout } from '@app/components/account/account-name'; import { useNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { StacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.models'; @@ -36,6 +37,7 @@ export const AccountListItem = memo(({ index, stacksAccount, onClose }: AccountL return ( } accountName={{name}} avatar={ } index={index} isSelected={false} - isPressable isLoading={false} onSelectAccount={onSelectAccount} /> diff --git a/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.tsx b/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.tsx index 2ef1aecb9df..f0f9e632948 100644 --- a/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.tsx +++ b/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.tsx @@ -1,20 +1,20 @@ +import { SwapSelectors } from '@tests/selectors/swap.selectors'; import { styled } from 'leather-styles/jsx'; import { formatMoneyWithoutSymbol } from '@app/common/money/format-money'; import { useGetFungibleTokenMetadataQuery } from '@app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query'; import { isFtAsset } from '@app/query/stacks/tokens/token-metadata.utils'; -import { Item } from '@app/ui/components/items/item'; -import { ItemDefaultLayout } from '@app/ui/components/items/item-default.layout'; +import { ItemInteractive } from '@app/ui/components/item/item-interactive'; +import { ItemLayout } from '@app/ui/components/item/item.layout'; import { useAlexSdkBalanceAsFiat } from '../../hooks/use-alex-sdk-fiat-price'; import { SwapAsset } from '../../hooks/use-swap-form'; interface SwapAssetItemProps { asset: SwapAsset; - dataTestId: string; onClick(): void; } -export function SwapAssetItem({ asset, dataTestId, onClick }: SwapAssetItemProps) { +export function SwapAssetItem({ asset, onClick }: SwapAssetItemProps) { const balanceAsFiat = useAlexSdkBalanceAsFiat(asset.balance, asset.price); const { data: ftMetadata } = useGetFungibleTokenMetadataQuery(asset.principal); @@ -22,17 +22,14 @@ export function SwapAssetItem({ asset, dataTestId, onClick }: SwapAssetItemProps const displayName = asset.displayName ?? ftMetadataName; return ( - - - } - contentLeftTop={{displayName}} - contentLeftBottom={{asset.name}} - contentRightTop={{formatMoneyWithoutSymbol(asset.balance)}} - contentRightBottom={{balanceAsFiat}} - /> - - + + } + titleLeft={displayName} + captionLeft={asset.name} + titleRight={formatMoneyWithoutSymbol(asset.balance)} + captionRight={balanceAsFiat} + /> + ); } diff --git a/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx b/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx index 5419a1c2629..32c2028fd89 100644 --- a/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx +++ b/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx @@ -1,6 +1,5 @@ import { useNavigate } from 'react-router-dom'; -import { SwapSelectors } from '@tests/selectors/swap.selectors'; import BigNumber from 'bignumber.js'; import { useFormikContext } from 'formik'; @@ -69,7 +68,6 @@ export function SwapAssetList({ assets }: SwapAssetList) { {selectableAssets.map(asset => ( onChooseAsset(asset)} /> diff --git a/src/app/ui/components/dowpdown-menu/dropdown-menu.tsx b/src/app/ui/components/dowpdown-menu/dropdown-menu.tsx index 169d56ed1c7..38d0bbf67ea 100644 --- a/src/app/ui/components/dowpdown-menu/dropdown-menu.tsx +++ b/src/app/ui/components/dowpdown-menu/dropdown-menu.tsx @@ -3,6 +3,8 @@ import { ReactNode, forwardRef } from 'react'; import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; import { css } from 'leather-styles/css'; +import { itemBaseStyles, itemInteractiveStyles } from '../item/item-interactive'; + export interface DropdownMenuItem { iconLeft?: ReactNode; iconRight?: ReactNode; @@ -67,20 +69,12 @@ const Label: typeof RadixDropdownMenu.Label = forwardRef((props, ref) => ( )); -const dropdownMenuItemStyles = css({ - bg: 'accent.background-primary', - color: 'accent.text-primary', - height: 'auto', - outline: 'none', - userSelect: 'none', - p: 'space.03', - - '&[data-highlighted]': { - bg: 'accent.component-background-hover', - }, -}); const Item: typeof RadixDropdownMenu.Item = forwardRef((props, ref) => ( - + )); const dropdownMenuSeparatorStyles = css({ diff --git a/src/app/ui/components/item/item-interactive.stories.tsx b/src/app/ui/components/item/item-interactive.stories.tsx new file mode 100644 index 00000000000..5a91cb3ce09 --- /dev/null +++ b/src/app/ui/components/item/item-interactive.stories.tsx @@ -0,0 +1,75 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { Box } from 'leather-styles/jsx'; + +import { Button } from '../button/button'; +import { BtcIcon } from '../icons/btc-icon'; +import { CopyIcon } from '../icons/copy-icon'; +import { QrCodeIcon } from '../icons/qr-code-icon'; +import { ItemInteractive as Component } from './item-interactive'; +import { ItemWithButtonsLayout } from './item-with-buttons.layout'; +import { ItemLayout } from './item.layout'; + +const meta: Meta = { + component: Component, + tags: ['autodocs'], + title: 'Item Interactive', +}; + +export default meta; +type Story = StoryObj; + +export const ItemInteractive: Story = { + render: () => ( + {}}> + + + ), +}; + +export const Default: Story = { + render: () => ( + {}}> + } + titleLeft="Label" + captionLeft="Caption" + titleRight="1,000.00 ABC" + captionRight="$1,000.00" + /> + + ), +}; + +export const Disabled: Story = { + render: () => ( + {}}> + } + titleLeft="Label" + captionLeft="Caption" + titleRight="1,000.00 ABC" + captionRight="$1,000.00" + /> + + ), +}; + +export const WithButtons: Story = { + render: () => ( + + } + title="Label" + caption="Caption" + buttons={ + <> + + + + } + /> + + ), +}; diff --git a/src/app/ui/components/item/item-interactive.tsx b/src/app/ui/components/item/item-interactive.tsx new file mode 100644 index 00000000000..f71ddd6d658 --- /dev/null +++ b/src/app/ui/components/item/item-interactive.tsx @@ -0,0 +1,99 @@ +import { forwardRef } from 'react'; + +import { type RecipeVariantProps, css, cva } from 'leather-styles/css'; +import { styled } from 'leather-styles/jsx'; +import { HTMLStyledProps } from 'leather-styles/types'; + +import { isDefined } from '@shared/utils'; + +const basePseudoOutlineProps = { + content: '""', + rounded: 'xs', + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + right: 0, +}; + +const focusVisibleStyles = { + _before: { + ...basePseudoOutlineProps, + border: '2px solid', + borderColor: 'lightModeBlue.500', + }, + _focusWithin: { outline: 'none' }, +}; + +export const itemBaseStyles = css.raw({ + bg: 'accent.background-primary', + color: 'accent.text-primary', + cursor: 'default', + display: 'flex', + height: 'auto', + outline: 'none', + p: 'space.03', + position: 'relative', + rounded: 'xs', + userSelect: 'none', + width: '100%', +}); + +export const itemInteractiveStyles = css.raw({ + cursor: 'pointer', + + '&:is(:active)': { + bg: 'accent.component-background-pressed', + }, + '&:is(:focus-visible)': { + ...focusVisibleStyles, + }, + '&:is(:disabled, [data-disabled])': { + _active: { bg: 'unset' }, + _focus: { border: 'unset' }, + _hover: { bg: 'unset' }, + color: 'accent.non-interactive', + cursor: 'not-allowed', + }, + '&:is(:hover, [data-highlighted])': { + _before: { borderColor: 'transparent' }, + bg: 'accent.component-background-hover', + }, +}); + +const itemRecipe = cva({ + base: itemBaseStyles, + variants: { + interactive: { + true: itemInteractiveStyles, + }, + }, +}); + +export const itemCaptionStyles = css({ + _groupDisabled: { color: 'accent.non-interactive' }, + color: 'accent.text-subdued', +}); + +export const itemChevronStyles = css({ + _groupDisabled: { color: 'accent.non-interactive' }, + color: 'accent.action-primary-default', +}); + +export type ItemVariantProps = RecipeVariantProps; + +export const ItemInteractive = forwardRef< + HTMLButtonElement, + HTMLStyledProps<'button'> & ItemVariantProps +>((props, ref) => { + const { onClick, ...rest } = props; + const isInteractive = isDefined(onClick); + return ( + + ); +}); diff --git a/src/app/ui/components/item/item-with-buttons.layout.tsx b/src/app/ui/components/item/item-with-buttons.layout.tsx new file mode 100644 index 00000000000..f1809838107 --- /dev/null +++ b/src/app/ui/components/item/item-with-buttons.layout.tsx @@ -0,0 +1,45 @@ +import { ReactNode } from 'react'; + +import { Flex, HStack, Stack, styled } from 'leather-styles/jsx'; + +import { Flag } from '@app/components/layout/flag'; + +import { itemCaptionStyles } from './item-interactive'; + +interface ItemWithButtonsLayoutProps { + buttons: ReactNode; + caption: string; + flagImg: ReactNode; + title: string; +} +export function ItemWithButtonsLayout({ + buttons, + caption, + flagImg, + title, +}: ItemWithButtonsLayoutProps) { + return ( + + + + + {title} + + + {caption} + + + + {buttons} + + + + ); +} diff --git a/src/app/ui/components/item/item.layout.tsx b/src/app/ui/components/item/item.layout.tsx new file mode 100644 index 00000000000..f21a68519a5 --- /dev/null +++ b/src/app/ui/components/item/item.layout.tsx @@ -0,0 +1,81 @@ +import { ReactNode, isValidElement } from 'react'; + +import { Flex, HStack, Stack, styled } from 'leather-styles/jsx'; + +import { Flag } from '@app/components/layout/flag'; + +import { CheckmarkIcon } from '../icons/checkmark-icon'; +import { ChevronUpIcon } from '../icons/chevron-up-icon'; +import { itemCaptionStyles, itemChevronStyles } from './item-interactive'; + +interface ItemLayoutProps { + captionLeft: ReactNode; + captionRight?: ReactNode; + flagImg: ReactNode; + isDisabled?: boolean; + isSelected?: boolean; + showChevron?: boolean; + titleLeft: ReactNode; + titleRight: ReactNode; +} +export function ItemLayout({ + captionLeft, + captionRight, + flagImg, + isSelected, + showChevron, + titleLeft, + titleRight, +}: ItemLayoutProps) { + return ( + + + + + {isValidElement(titleLeft) ? ( + titleLeft + ) : ( + + {titleLeft} + + )} + {isSelected && } + + {isValidElement(captionLeft) ? ( + captionLeft + ) : ( + + {captionLeft} + + )} + + + + {isValidElement(titleRight) ? ( + titleRight + ) : ( + + {titleRight} + + )} + {isValidElement(captionRight) ? ( + captionRight + ) : ( + + {captionRight} + + )} + + {showChevron && } + + + + ); +} diff --git a/src/app/ui/components/items/item-default.layout.tsx b/src/app/ui/components/items/item-default.layout.tsx deleted file mode 100644 index ad9b7b0e7f5..00000000000 --- a/src/app/ui/components/items/item-default.layout.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { ReactNode } from 'react'; - -import { Grid, GridItem } from 'leather-styles/jsx'; - -import { ChevronUpIcon } from '../icons/chevron-up-icon'; - -interface ItemDefaultLayoutProps { - contentLeftTop: ReactNode; - contentLeftBottom?: ReactNode; - contentRightTop: ReactNode; - contentRightBottom?: ReactNode; - flagImg?: ReactNode; - isDisabled?: boolean; - isPressable?: boolean; -} -export function ItemDefaultLayout({ - contentLeftTop, - contentLeftBottom, - contentRightTop, - contentRightBottom, - flagImg, - isDisabled, - isPressable, -}: ItemDefaultLayoutProps) { - return ( - - - {flagImg} - - - {contentLeftTop} - - - {contentRightTop} - - - {isPressable && ( - - )} - - {/* Using scroll here for now bc possibly shows addresses */} - - {contentLeftBottom} - - {contentRightBottom && {contentRightBottom}} - - ); -} diff --git a/src/app/ui/components/items/item-with-buttons.layout.tsx b/src/app/ui/components/items/item-with-buttons.layout.tsx deleted file mode 100644 index 7900957c59e..00000000000 --- a/src/app/ui/components/items/item-with-buttons.layout.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { ReactNode } from 'react'; - -import { Grid, GridItem } from 'leather-styles/jsx'; - -interface ItemWithButtonsLayoutProps { - contentLeftTop: ReactNode; - contentLeftBottom?: ReactNode; - contentRightTop: ReactNode; - contentRightBottom?: ReactNode; - flagImg?: ReactNode; - isDisabled?: boolean; - isPressable?: boolean; -} -export function ItemWithButtonsLayout({ - contentLeftTop, - contentLeftBottom, - contentRightTop, - flagImg, -}: ItemWithButtonsLayoutProps) { - return ( - - - {flagImg} - - - {contentLeftTop} - - - {contentRightTop} - - {/* Using scroll here for now bc possibly shows addresses */} - - {contentLeftBottom} - - - ); -} diff --git a/src/app/ui/components/items/item.stories.tsx b/src/app/ui/components/items/item.stories.tsx deleted file mode 100644 index f739c1ce5b9..00000000000 --- a/src/app/ui/components/items/item.stories.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { HStack } from 'leather-styles/jsx'; - -import { Button } from '../button/button'; -import { BtcIcon } from '../icons/btc-icon'; -import { CheckmarkIcon } from '../icons/checkmark-icon'; -import { CopyIcon } from '../icons/copy-icon'; -import { QrCodeIcon } from '../icons/qr-code-icon'; -import { Item } from './item'; -import { ItemDefaultLayout } from './item-default.layout'; -import { ItemWithButtonsLayout } from './item-with-buttons.layout'; - -const meta: Meta = { - component: Item.Root, - tags: ['autodocs'], - title: 'Item', -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - render: () => ( - // eslint-disable-next-line no-console - - - } - contentLeftTop={Label} - contentLeftBottom={Caption} - contentRightTop={1,000.00 ABC} - contentRightBottom={$1,000.00} - /> - - - ), -}; - -export const DefaultPressable: Story = { - render: () => ( - // eslint-disable-next-line no-console - console.log(`I'm interactive`)}> - - } - contentLeftTop={Label} - contentLeftBottom={Caption} - contentRightTop={1,000.00 ABC} - contentRightBottom={$1,000.00} - /> - - - ), -}; - -export const Selected: Story = { - render: () => ( - // eslint-disable-next-line no-console - console.log(`I'm interactive`)}> - - } - contentLeftTop={ - - Label - - - } - contentLeftBottom={Caption} - contentRightTop={1,000.00 ABC} - contentRightBottom={$1,000.00} - /> - - - ), -}; - -export const Disabled: Story = { - render: () => ( - // eslint-disable-next-line no-console - console.log(`I'm interactive`)}> - - } - contentLeftTop={Label} - contentLeftBottom={Caption} - contentRightTop={1,000.00 ABC} - contentRightBottom={$1,000.00} - /> - - - ), -}; - -export const WithButtons: Story = { - render: () => ( - // eslint-disable-next-line no-console - - - } - contentLeftTop={Label} - contentLeftBottom={Caption} - contentRightTop={ - - - - - } - /> - - - ), -}; - -export const WithButtonsDisabled: Story = { - render: () => ( - // eslint-disable-next-line no-console - - - } - contentLeftTop={Label} - contentLeftBottom={Caption} - contentRightTop={ - - - - - } - /> - - - ), -}; diff --git a/src/app/ui/components/items/item.tsx b/src/app/ui/components/items/item.tsx deleted file mode 100644 index eec33764e1e..00000000000 --- a/src/app/ui/components/items/item.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { type ComponentProps, KeyboardEvent } from 'react'; - -import { sva } from 'leather-styles/css'; -import { BoxProps, styled } from 'leather-styles/jsx'; - -import { createStyleContext } from '@app/ui/utils/style-context'; - -const item = sva({ - slots: ['root', 'content', 'text', 'caption'], - base: { - root: { - bg: 'accent.background-primary', - height: 'auto', - userSelect: 'none', - p: 'space.03', - width: '100%', - }, - content: { - width: '100%', - }, - text: { - color: 'accent.text-primary', - fontWeight: 500, - textStyle: 'label.02', - }, - caption: { - color: 'accent.text-subdued', - }, - }, - variants: { - isDisabled: { - true: { - root: { - _active: { bg: 'unset' }, - _focus: { border: 'unset' }, - _hover: { bg: 'unset' }, - cursor: 'not-allowed', - }, - content: {}, - text: { - color: 'accent.non-interactive', - }, - caption: { - color: 'accent.non-interactive', - }, - }, - }, - isPressable: { - true: { - root: { - _active: { - bg: 'accent.component-background-pressed', - }, - _focus: { - border: '2px solid {focus}', - }, - _hover: { - bg: 'accent.component-background-hover', - }, - }, - }, - }, - isSelected: { - true: { - root: { - _active: { - bg: 'unset', - }, - _focus: { - border: 'unset', - }, - _hover: { - bg: 'unset', - }, - }, - }, - }, - }, - compoundVariants: [ - { - isDisabled: true, - isPressable: true, - css: { - root: { - _active: { bg: 'unset' }, - _focus: { border: 'unset' }, - _hover: { bg: 'unset' }, - cursor: 'not-allowed', - }, - }, - }, - ], -}); - -const { withProvider, withContext } = createStyleContext(item); - -const RootBase = withProvider('div', 'root'); - -interface RootProps extends ComponentProps<'div'> { - isDisabled?: boolean; - isPressable?: boolean; - isSelected?: boolean; - onClick?(): void; -} -function Root({ isDisabled, isPressable, isSelected, onClick, ...props }: RootProps & BoxProps) { - const isRoleButton = !!onClick; - - function onKeyDown(e: KeyboardEvent) { - if (!isRoleButton) return; - if (e.key === 'enter' || e.key === ' ') { - e.preventDefault(); - onClick(); - } - } - - return ( - - ); -} - -const Content = withContext(styled('div'), 'content'); - -const Text = withContext(styled('span'), 'text'); - -const Caption = withContext(styled('span'), 'caption'); - -export const Item = { Root, Content, Text, Caption }; diff --git a/src/app/ui/components/select/select.tsx b/src/app/ui/components/select/select.tsx index 9953bdce076..166fd0b70e0 100644 --- a/src/app/ui/components/select/select.tsx +++ b/src/app/ui/components/select/select.tsx @@ -3,6 +3,8 @@ import { ReactNode, forwardRef } from 'react'; import * as RadixSelect from '@radix-ui/react-select'; import { css } from 'leather-styles/css'; +import { itemBaseStyles, itemInteractiveStyles } from '../item/item-interactive'; + export interface SelectItem { iconLeft?: ReactNode; iconRight?: ReactNode; @@ -79,20 +81,8 @@ const Label: typeof RadixSelect.Label = forwardRef((props, ref) => ( )); -const selectItemStyles = css({ - bg: 'accent.background-primary', - color: 'accent.text-primary', - height: 'auto', - outline: 'none', - userSelect: 'none', - p: 'space.03', - - '&[data-highlighted]': { - bg: 'accent.component-background-hover', - }, -}); const Item: typeof RadixSelect.Item = forwardRef((props, ref) => ( - + )); const ItemText = RadixSelect.ItemText; diff --git a/src/app/ui/components/typography/caption.tsx b/src/app/ui/components/typography/caption.tsx index 96eb6c55b1b..d87df4d218e 100644 --- a/src/app/ui/components/typography/caption.tsx +++ b/src/app/ui/components/typography/caption.tsx @@ -1,9 +1,17 @@ import { forwardRef } from 'react'; -import { BoxProps, styled } from 'leather-styles/jsx'; +import { HTMLStyledProps, styled } from 'leather-styles/jsx'; -export const Caption = forwardRef(({ children, ...props }, ref) => ( - - {children} - -)); +export const Caption = forwardRef>( + ({ children, ...props }, ref) => ( + + {children} + + ) +); diff --git a/src/app/ui/components/typography/title.tsx b/src/app/ui/components/typography/title.tsx index 0129f184d97..744ace4763e 100644 --- a/src/app/ui/components/typography/title.tsx +++ b/src/app/ui/components/typography/title.tsx @@ -1,15 +1,17 @@ import { forwardRef } from 'react'; -import { BoxProps, styled } from 'leather-styles/jsx'; +import { HTMLStyledProps, styled } from 'leather-styles/jsx'; -export const Title = forwardRef(({ children, ...props }, ref) => ( - - {children} - -)); +export const Title = forwardRef>( + ({ children, ...props }, ref) => ( + + {children} + + ) +);