From 54d48e3fd1d1e243cd06980cba35cf5f9fba4f2d Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 29 Sep 2025 16:45:08 +0200 Subject: [PATCH 1/7] feat: migrate DetailInfo to mui --- .../curve-ui-kit/src/shared/ui/ActionInfo.tsx | 34 ++--- packages/ui/src/DetailInfo/DetailInfo.tsx | 121 +++++------------- 2 files changed, 49 insertions(+), 106 deletions(-) diff --git a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx index af487dc5a3..cbe823cad3 100644 --- a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx +++ b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx @@ -7,7 +7,7 @@ import AlertTitle from '@mui/material/AlertTitle' import IconButton from '@mui/material/IconButton' import Link from '@mui/material/Link' import Snackbar from '@mui/material/Snackbar' -import Stack from '@mui/material/Stack' +import Stack, { type StackProps } from '@mui/material/Stack' import Typography, { type TypographyProps } from '@mui/material/Typography' import { useSwitch } from '@ui-kit/hooks/useSwitch' import { t } from '@ui-kit/lib/i18n' @@ -16,22 +16,22 @@ import { RouterLink } from '@ui-kit/shared/ui/RouterLink' import { Duration } from '@ui-kit/themes/design/0_primitives' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' import type { TypographyVariantKey } from '@ui-kit/themes/typography' -import { copyToClipboard, type SxProps } from '@ui-kit/utils' +import { copyToClipboard } from '@ui-kit/utils' import { Tooltip } from './Tooltip' import { WithSkeleton } from './WithSkeleton' const { Spacing, IconSize } = SizesAndSpaces const MOCK_SKELETON = 10 // Mock value for skeleton to infer some width -type ComponentSize = 'small' | 'medium' | 'large' +export type ActionInfoSize = 'small' | 'medium' | 'large' -export type ActionInfoProps = { +export type ActionInfoProps = Pick & { /** Label displayed on the left side */ - label: string + label: ReactNode /** Custom color for the label text */ labelColor?: TypographyProps['color'] /** Primary value to display and copy */ - value: string + value: ReactNode /** Custom color for the value text */ valueColor?: TypographyProps['color'] /** Optional content to display to the left of the value */ @@ -46,39 +46,41 @@ export type ActionInfoProps = { prevValueColor?: TypographyProps['color'] /** URL to navigate to when clicking the external link button */ link?: string - /** Whether or not the value can be copied */ + /** Whether the value can be copied. It also requires a `copyValue` or `value` to be a string */ copy?: boolean - /** Value to be copied. Example use case would be copying the full address when the value is formatted / shortened. Defaults to original value. */ + /** Value to be copied. + * Example: copy the full address or amount when `value` is formatted. + * Defaults to the original value (if it's a string!). */ copyValue?: string /** Message displayed in the snackbar title when the value is copied */ copiedTitle?: string /** Size of the component */ - size?: ComponentSize + size?: ActionInfoSize /** Whether the component is in a loading state. Can be boolean or string (string value is used for skeleton width inference) */ loading?: boolean | string /** Error state; Unused for now, but kept for future use */ error?: boolean | Error | null + /** Test ID for the component */ testId?: string - sx?: SxProps } const labelSize = { small: 'bodyXsRegular', medium: 'bodyMRegular', large: 'bodyMRegular', -} as const satisfies Record +} as const satisfies Record const prevValueSize = { small: 'bodySRegular', medium: 'bodyMRegular', large: 'bodyMRegular', -} as const satisfies Record +} as const satisfies Record const valueSize = { small: 'bodyXsBold', medium: 'highlightM', large: 'headingSBold', -} as const satisfies Record +} as const satisfies Record const ActionInfo = ({ label, @@ -98,10 +100,10 @@ const ActionInfo = ({ loading = false, error = false, testId = 'action-info', - sx, + ...styleProps // sx, className }: ActionInfoProps) => { const [isSnackbarOpen, openSnackbar, closeSnackbar] = useSwitch(false) - const copyValue = (givenCopyValue ?? value).trim() + const copyValue = (givenCopyValue ?? (typeof value === 'string' ? value : '')).trim() const copyAndShowSnackbar = useCallback(() => { void copyToClipboard(copyValue) @@ -109,7 +111,7 @@ const ActionInfo = ({ }, [copyValue, openSnackbar]) return ( - + {label} diff --git a/packages/ui/src/DetailInfo/DetailInfo.tsx b/packages/ui/src/DetailInfo/DetailInfo.tsx index 20a720fe89..aa8cba05a3 100644 --- a/packages/ui/src/DetailInfo/DetailInfo.tsx +++ b/packages/ui/src/DetailInfo/DetailInfo.tsx @@ -1,11 +1,18 @@ import { ReactNode } from 'react' import { styled } from 'styled-components' -import Box from 'ui/src/Box/Box' -import Loader from 'ui/src/Loader/Loader' +import Divider from '@mui/material/Divider' +import ActionInfo, { ActionInfoSize } from '@ui-kit/shared/ui/ActionInfo' type Variant = 'error' | 'warning' | 'success' | '' type Size = 'xs' | 'sm' | 'md' | 'lg' +const SizeMap = { + xs: 'small', + sm: 'small', + md: 'medium', + lg: 'large', +} satisfies Record + type Props = { children: ReactNode action?: ReactNode @@ -35,37 +42,29 @@ const DetailInfo = ({ children, size = 'sm', ...props -}: Props) => { - const classNames = `${className} ${isDivider ? 'divider' : ''}` - - return ( - ( + <> + {isDivider && } + - {label && {label}} - - {loading && } - {!loading && ( - <> - {children || '-'} {!!Tooltip && Tooltip} {Action && Action} - - )} - - - ) -} - -export const DetailLabel = styled.span` - display: inline-block; - font-weight: bold; -` + className={className} + size={SizeMap[size]} + loading={loading} + label={label} + copy={typeof children === 'string'} + value={ + + {!loading && ( + <> + {children || '-'} {!!Tooltip && Tooltip} {Action && Action} + + )} + + } + copyValue={typeof children === 'string' ? children : ''} + /> + +) type DetailValeProps = { haveLabel: boolean @@ -94,62 +93,4 @@ const DetailValue = styled.div` }}; ` -interface WrapperProps extends Pick {} - -const Wrapper = styled(Box)` - align-items: center; - min-height: 1.7rem; // 27px - - ${({ size }) => { - if (size === 'sm') { - return `font-size: var(--font-size-2);` - } else if (size === 'md') { - return `font-size: var(--font-size-3);` - } - }} - - .svg-tooltip { - margin-top: 0.25rem; - top: 0.1rem; - } - - .svg-arrow { - position: relative; - top: 0.1875rem; // 3px - opacity: 0.7; - } - - ${({ isDivider }) => { - if (isDivider) { - return ` - margin-top: var(--spacing-1); - padding-top: var(--spacing-1); - border-color: inherit; - border-top: 1px solid var(--border-400); - ` - } - }} - ${({ isMultiLine }) => { - if (isMultiLine) { - return ` - grid-auto-flow: row; - ` - } - }} - ${DetailValue} { - ${({ textLeft }) => { - if (textLeft) { - return ` - justify-content: flex-start; - text-align: left; - ` - } - }} - } - - .svg-tooltip { - top: 0.2rem; - } -` - export default DetailInfo From ee61195e880db77b56508430ee5bd8303622acd8 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 29 Sep 2025 21:05:54 +0200 Subject: [PATCH 2/7] refactor: cleanup DetailInfo --- .../LoanInfoLlamma/LoanInfoParameters.tsx | 8 +- .../curve-ui-kit/src/shared/ui/ActionInfo.tsx | 10 ++- packages/ui/src/DetailInfo/DetailInfo.tsx | 73 +++++-------------- 3 files changed, 28 insertions(+), 63 deletions(-) diff --git a/apps/main/src/loan/components/LoanInfoLlamma/LoanInfoParameters.tsx b/apps/main/src/loan/components/LoanInfoLlamma/LoanInfoParameters.tsx index 7b7e17087e..ec65422860 100644 --- a/apps/main/src/loan/components/LoanInfoLlamma/LoanInfoParameters.tsx +++ b/apps/main/src/loan/components/LoanInfoLlamma/LoanInfoParameters.tsx @@ -15,10 +15,10 @@ const LoanInfoParameters = ({ llamma, llammaId }: Props) => { return ( - + {formatNumber(llamma?.A, { useGrouping: false })} - + {typeof loanDetails?.basePrice !== 'undefined' && ( { )} - + {typeof priceInfo?.oraclePrice !== 'undefined' && ( { )} - + {typeof parameters?.rate !== 'undefined' && ( & { /** Optional content to display to the right of the value */ valueRight?: ReactNode /** Tooltip text to display when hovering over the value */ - valueTooltip?: string + valueTooltip?: ReactNode /** Previous value (if needed for comparison) */ prevValue?: string /** Custom color for the previous value text */ @@ -56,8 +56,12 @@ export type ActionInfoProps = Pick & { copiedTitle?: string /** Size of the component */ size?: ActionInfoSize - /** Whether the component is in a loading state. Can be boolean or string (string value is used for skeleton width inference) */ - loading?: boolean | string + /** Whether the component is in a loading state. Can be one of: + * - boolean + * - string (value is used for skeleton width inference) + * - [number, number] (explicit skeleton width and height in px) + **/ + loading?: boolean | [number, number] | string /** Error state; Unused for now, but kept for future use */ error?: boolean | Error | null /** Test ID for the component */ diff --git a/packages/ui/src/DetailInfo/DetailInfo.tsx b/packages/ui/src/DetailInfo/DetailInfo.tsx index aa8cba05a3..6222af7aaf 100644 --- a/packages/ui/src/DetailInfo/DetailInfo.tsx +++ b/packages/ui/src/DetailInfo/DetailInfo.tsx @@ -1,21 +1,21 @@ import { ReactNode } from 'react' -import { styled } from 'styled-components' import Divider from '@mui/material/Divider' -import ActionInfo, { ActionInfoSize } from '@ui-kit/shared/ui/ActionInfo' +import ActionInfo, { ActionInfoProps } from '@ui-kit/shared/ui/ActionInfo' +import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' + +const { Spacing } = SizesAndSpaces type Variant = 'error' | 'warning' | 'success' | '' -type Size = 'xs' | 'sm' | 'md' | 'lg' -const SizeMap = { - xs: 'small', - sm: 'small', - md: 'medium', - lg: 'large', -} satisfies Record +const VariantToColorMap = { + '': 'textPrimary', + error: 'error', + warning: 'warning', + success: 'success', +} satisfies Record type Props = { children: ReactNode - action?: ReactNode className?: string isBold?: boolean | null isDivider?: boolean @@ -23,74 +23,35 @@ type Props = { label?: ReactNode loading?: boolean loadingSkeleton?: [number, number] - size?: Size - textLeft?: boolean tooltip?: ReactNode variant?: Variant } const DetailInfo = ({ - action: Action, className, isBold, isDivider, label, loading, loadingSkeleton, - tooltip: Tooltip, + tooltip, variant, children, - size = 'sm', - ...props }: Props) => ( <> - {isDivider && } + {isDivider && } - {!loading && ( - <> - {children || '-'} {!!Tooltip && Tooltip} {Action && Action} - - )} - - } + error={variant === 'error'} + loading={loading && (loadingSkeleton || true)} copyValue={typeof children === 'string' ? children : ''} /> ) -type DetailValeProps = { - haveLabel: boolean - isBold?: boolean | null - variant?: Variant -} - -const DetailValue = styled.div` - align-items: center; - display: flex; - justify-content: flex-end; - - font-weight: ${({ isBold }) => (isBold ? '700' : 'inherit')}; - text-align: ${({ haveLabel }) => (haveLabel ? 'right' : 'left')}; - - color: ${({ variant }) => { - if (variant === 'error') { - return 'var(--danger-400)' - } else if (variant === 'warning') { - return 'var(--warning-text-400)' - } else if (variant === 'success') { - return 'var(--success-400)' - } else { - return 'inherit' - } - }}; -` - export default DetailInfo From cc3d4fa7ae90cf3b71cbf17d1d92bbbb37687627 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 30 Sep 2025 14:45:30 +0200 Subject: [PATCH 3/7] feat: restore old DetailInfo for stable mode --- packages/ui/src/DetailInfo/DetailInfo.tsx | 118 +++++++++++++++++++++- 1 file changed, 116 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/DetailInfo/DetailInfo.tsx b/packages/ui/src/DetailInfo/DetailInfo.tsx index 6222af7aaf..f6951e8e2b 100644 --- a/packages/ui/src/DetailInfo/DetailInfo.tsx +++ b/packages/ui/src/DetailInfo/DetailInfo.tsx @@ -1,7 +1,12 @@ import { ReactNode } from 'react' +import { styled } from 'styled-components' import Divider from '@mui/material/Divider' +import { useReleaseChannel } from '@ui-kit/hooks/useLocalStorage' import ActionInfo, { ActionInfoProps } from '@ui-kit/shared/ui/ActionInfo' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' +import { ReleaseChannel } from '@ui-kit/utils' +import Box from 'ui/src/Box/Box' +import Loader from 'ui/src/Loader/Loader' const { Spacing } = SizesAndSpaces @@ -27,7 +32,38 @@ type Props = { variant?: Variant } -const DetailInfo = ({ +const OldDetailInfo = ({ + className, + isBold, + isDivider, + label, + loading, + loadingSkeleton, + tooltip, + variant, + children, +}: Props) => ( + + {label && {label}} + + {loading && } + {!loading && ( + <> + {children || '-'} {!!tooltip && tooltip} + + )} + + +) + +const NewDetailInfo = ({ className, isBold, isDivider, @@ -50,8 +86,86 @@ const DetailInfo = ({ error={variant === 'error'} loading={loading && (loadingSkeleton || true)} copyValue={typeof children === 'string' ? children : ''} + size={isBold ? 'large' : 'medium'} /> ) -export default DetailInfo +export const DetailLabel = styled.span` + display: inline-block; + font-weight: bold; +` + +type DetailValeProps = { + haveLabel: boolean + isBold?: boolean | null + variant?: Variant +} + +const DetailValue = styled.div` + align-items: center; + display: flex; + justify-content: flex-end; + + font-weight: ${({ isBold }) => (isBold ? '700' : 'inherit')}; + text-align: ${({ haveLabel }) => (haveLabel ? 'right' : 'left')}; + + color: ${({ variant }) => { + if (variant === 'error') { + return 'var(--danger-400)' + } else if (variant === 'warning') { + return 'var(--warning-text-400)' + } else if (variant === 'success') { + return 'var(--success-400)' + } else { + return 'inherit' + } + }}; +` + +interface WrapperProps extends Pick {} + +const Wrapper = styled(Box)` + align-items: center; + min-height: 1.7rem; // 27px + font-size: var(--font-size-3); + + .svg-tooltip { + margin-top: 0.25rem; + top: 0.1rem; + } + + .svg-arrow { + position: relative; + top: 0.1875rem; // 3px + opacity: 0.7; + } + + ${({ isDivider }) => { + if (isDivider) { + return ` + margin-top: var(--spacing-1); + padding-top: var(--spacing-1); + border-color: inherit; + border-top: 1px solid var(--border-400); + ` + } + }} + ${({ isMultiLine }) => { + if (isMultiLine) { + return ` + grid-auto-flow: row; + ` + } + }} + + .svg-tooltip { + top: 0.2rem; + } +` + +export default function DetailInfo(props: Props) { + const [releaseChannel] = useReleaseChannel() + const DetailInfo = releaseChannel === ReleaseChannel.Beta ? NewDetailInfo : OldDetailInfo + return +} From 009b3aba59fdf56768c0981cefa64dd8200c4d21 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 30 Sep 2025 14:56:57 +0200 Subject: [PATCH 4/7] chore: self-review --- packages/ui/src/DetailInfo/DetailInfo.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/ui/src/DetailInfo/DetailInfo.tsx b/packages/ui/src/DetailInfo/DetailInfo.tsx index f6951e8e2b..e1162fbdb9 100644 --- a/packages/ui/src/DetailInfo/DetailInfo.tsx +++ b/packages/ui/src/DetailInfo/DetailInfo.tsx @@ -12,6 +12,10 @@ const { Spacing } = SizesAndSpaces type Variant = 'error' | 'warning' | 'success' | '' +/** + * Maps the `variant` prop to the corresponding `valueColor` prop for the `ActionInfo` component. + * This should be removed when we get rid of this file and use `ActionInfo` directly. + * */ const VariantToColorMap = { '': 'textPrimary', error: 'error', From 7aaa7f6936f8ca5430637694dd5dddf0180dfb96 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 30 Sep 2025 15:00:46 +0200 Subject: [PATCH 5/7] fix: skeleton size --- packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx index 93424cbb83..2628387d8c 100644 --- a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx +++ b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx @@ -146,7 +146,10 @@ const ActionInfo = ({ {valueLeft} - + {loading ? ( typeof loading === 'string' ? ( From 1012b1288c55c8dcb0d5d95f885c2ee64465edce Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Wed, 1 Oct 2025 15:43:15 +0200 Subject: [PATCH 6/7] fix: review comments --- .../components/DetailInfoEstLpTokens.tsx | 1 - .../Statistics/AdvancedDetails.tsx | 2 +- .../components/PegKeeperAdvancedDetails.tsx | 2 -- .../curve-ui-kit/src/shared/ui/ActionInfo.tsx | 22 ++++++------- .../shared/ui/stories/ActionInfo.stories.tsx | 10 +++--- packages/ui/src/DetailInfo/DetailInfo.tsx | 31 +++---------------- 6 files changed, 18 insertions(+), 50 deletions(-) diff --git a/apps/main/src/dex/components/PagePool/components/DetailInfoEstLpTokens.tsx b/apps/main/src/dex/components/PagePool/components/DetailInfoEstLpTokens.tsx index a72215a53e..3f4df2f78c 100644 --- a/apps/main/src/dex/components/PagePool/components/DetailInfoEstLpTokens.tsx +++ b/apps/main/src/dex/components/PagePool/components/DetailInfoEstLpTokens.tsx @@ -34,7 +34,6 @@ const DetailInfoEstLpTokens = ({ isBold loading={formLpTokenExpected.loading} loadingSkeleton={[85, 23]} - className={formLpTokenExpected.expected.length > 20 ? 'isRow' : ''} label={t`Minimum LP Tokens:`} tooltip={ showTooltip ? ( diff --git a/apps/main/src/loan/components/PageCrvUsdStaking/Statistics/AdvancedDetails.tsx b/apps/main/src/loan/components/PageCrvUsdStaking/Statistics/AdvancedDetails.tsx index fdd4e476de..9aaf0c9ebd 100644 --- a/apps/main/src/loan/components/PageCrvUsdStaking/Statistics/AdvancedDetails.tsx +++ b/apps/main/src/loan/components/PageCrvUsdStaking/Statistics/AdvancedDetails.tsx @@ -20,7 +20,7 @@ const AdvancedDetails = () => ( label={t`Vault Contract Address`} value={shortenAddress(SCRVUSD_VAULT_ADDRESS)} link={networks[Chain.Ethereum].scanAddressPath(SCRVUSD_VAULT_ADDRESS)} - copy + copyValue={SCRVUSD_VAULT_ADDRESS} copiedTitle={t`Vault contract address copied!`} /> diff --git a/apps/main/src/loan/components/PagePegKeepers/components/PegKeeperAdvancedDetails.tsx b/apps/main/src/loan/components/PagePegKeepers/components/PegKeeperAdvancedDetails.tsx index 7498bf8796..39fe76a683 100644 --- a/apps/main/src/loan/components/PagePegKeepers/components/PegKeeperAdvancedDetails.tsx +++ b/apps/main/src/loan/components/PagePegKeepers/components/PegKeeperAdvancedDetails.tsx @@ -53,7 +53,6 @@ export const PegKeeperAdvancedDetails = ({ label={t`Pool`} value={shortenAddress(poolAddress, { digits: 2 })} link={getInternalUrl('dex', 'ethereum', `${DEX_ROUTES.PAGE_POOLS}/${poolId}/deposit`)} - copy copyValue={poolAddress} testId={`${testId}-action-info-pool`} /> @@ -62,7 +61,6 @@ export const PegKeeperAdvancedDetails = ({ label={t`Contract`} value={shortenAddress(address, { digits: 2 })} link={`https://etherscan.io/address/${address}`} - copy copyValue={address} testId={`${testId}-action-info-contract`} /> diff --git a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx index 2628387d8c..6167060ad6 100644 --- a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx +++ b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx @@ -25,7 +25,7 @@ const MOCK_SKELETON = 10 // Mock value for skeleton to infer some width export type ActionInfoSize = 'small' | 'medium' | 'large' -export type ActionInfoProps = Pick & { +export type ActionInfoProps = { /** Label displayed on the left side */ label: ReactNode /** Custom color for the label text */ @@ -46,11 +46,7 @@ export type ActionInfoProps = Pick & { prevValueColor?: TypographyProps['color'] /** URL to navigate to when clicking the external link button */ link?: string - /** Whether the value can be copied. It also requires a `copyValue` or `value` to be a string */ - copy?: boolean - /** Value to be copied. - * Example: copy the full address or amount when `value` is formatted. - * Defaults to the original value (if it's a string!). */ + /** Value to be copied (will display a copy button). */ copyValue?: string /** Message displayed in the snackbar title when the value is copied */ copiedTitle?: string @@ -66,6 +62,8 @@ export type ActionInfoProps = Pick & { error?: boolean | Error | null /** Test ID for the component */ testId?: string + /** Additional styles */ + sx?: StackProps['sx'] } const labelSize = { @@ -98,24 +96,22 @@ const ActionInfo = ({ valueTooltip = '', link, size = 'medium', - copy = false, - copyValue: givenCopyValue, + copyValue, copiedTitle, loading = false, error = false, testId = 'action-info', - ...styleProps // sx, className + sx, }: ActionInfoProps) => { const [isSnackbarOpen, openSnackbar, closeSnackbar] = useSwitch(false) - const copyValue = (givenCopyValue ?? (typeof value === 'string' ? value : '')).trim() const copyAndShowSnackbar = useCallback(() => { - void copyToClipboard(copyValue) + void copyToClipboard(copyValue!.trim()) openSnackbar() }, [copyValue, openSnackbar]) return ( - + {label} @@ -169,7 +165,7 @@ const ActionInfo = ({ - {copy && copyValue && ( + {copyValue && ( diff --git a/packages/curve-ui-kit/src/shared/ui/stories/ActionInfo.stories.tsx b/packages/curve-ui-kit/src/shared/ui/stories/ActionInfo.stories.tsx index be45a2ac2e..86cad9c3f4 100644 --- a/packages/curve-ui-kit/src/shared/ui/stories/ActionInfo.stories.tsx +++ b/packages/curve-ui-kit/src/shared/ui/stories/ActionInfo.stories.tsx @@ -45,9 +45,9 @@ const meta: Meta = { control: 'text', description: 'The URL to navigate to when clicking the external link button', }, - copy: { - control: 'boolean', - description: 'Whether or not the value can be copied', + copyValue: { + control: 'text', + description: 'The value to be copied (will display a copy button)', }, copiedTitle: { control: 'text', @@ -73,7 +73,7 @@ const meta: Meta = { valueColor: 'textPrimary', valueTooltip: 'Contract address', link: 'https://etherscan.io/address/0x0655977feb2f289a4ab78af67bab0d17aab84367', - copy: true, + copyValue: '', copiedTitle: 'Contract address copied!', size: 'small', loading: false, @@ -172,7 +172,6 @@ export const WithEmptyValueAndSwitch: Story = { value: '', valueRight: , link: '', - copy: false, size: 'medium', }, parameters: { @@ -202,7 +201,6 @@ export const WithError: Story = { args: { error: new Error('Failed to load contract address'), size: 'medium', - copy: false, link: '', }, parameters: { diff --git a/packages/ui/src/DetailInfo/DetailInfo.tsx b/packages/ui/src/DetailInfo/DetailInfo.tsx index e1162fbdb9..9a0f8555e7 100644 --- a/packages/ui/src/DetailInfo/DetailInfo.tsx +++ b/packages/ui/src/DetailInfo/DetailInfo.tsx @@ -25,7 +25,6 @@ const VariantToColorMap = { type Props = { children: ReactNode - className?: string isBold?: boolean | null isDivider?: boolean isMultiLine?: boolean @@ -36,19 +35,9 @@ type Props = { variant?: Variant } -const OldDetailInfo = ({ - className, - isBold, - isDivider, - label, - loading, - loadingSkeleton, - tooltip, - variant, - children, -}: Props) => ( +const OldDetailInfo = ({ isBold, isDivider, label, loading, loadingSkeleton, tooltip, variant, children }: Props) => ( ) -const NewDetailInfo = ({ - className, - isBold, - isDivider, - label, - loading, - loadingSkeleton, - tooltip, - variant, - children, -}: Props) => ( +const NewDetailInfo = ({ isBold, isDivider, label, loading, loadingSkeleton, tooltip, variant, children }: Props) => ( <> {isDivider && } ) From 62d768401ed896d001b850a2b3f77b9ac7412ddd Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Wed, 1 Oct 2025 16:28:34 +0200 Subject: [PATCH 7/7] feat: show error and value when possible this is used for example in the high price impact error. --- .../curve-ui-kit/src/shared/ui/ActionInfo.tsx | 24 +++++------ .../src/shared/ui/WithSkeleton.tsx | 40 +++++-------------- .../src/shared/ui/WithTooltip.tsx | 10 +++++ .../src/shared/ui/WithWrapper.tsx | 17 ++++++++ 4 files changed, 48 insertions(+), 43 deletions(-) create mode 100644 packages/curve-ui-kit/src/shared/ui/WithTooltip.tsx create mode 100644 packages/curve-ui-kit/src/shared/ui/WithWrapper.tsx diff --git a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx index 6167060ad6..c508cf1eb4 100644 --- a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx +++ b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx @@ -13,6 +13,7 @@ import { useSwitch } from '@ui-kit/hooks/useSwitch' import { t } from '@ui-kit/lib/i18n' import { ExclamationTriangleIcon } from '@ui-kit/shared/icons/ExclamationTriangleIcon' import { RouterLink } from '@ui-kit/shared/ui/RouterLink' +import { WithTooltip } from '@ui-kit/shared/ui/WithTooltip' import { Duration } from '@ui-kit/themes/design/0_primitives' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' import type { TypographyVariantKey } from '@ui-kit/themes/typography' @@ -59,7 +60,7 @@ export type ActionInfoProps = { **/ loading?: boolean | [number, number] | string /** Error state; Unused for now, but kept for future use */ - error?: boolean | Error | null + error?: boolean | Error | string | null /** Test ID for the component */ testId?: string /** Additional styles */ @@ -110,6 +111,7 @@ const ActionInfo = ({ openSnackbar() }, [copyValue, openSnackbar]) + const errorMessage = (typeof error === 'object' && error?.message) || (typeof error === 'string' && error) return ( @@ -137,7 +139,7 @@ const ActionInfo = ({ /> )} - + {/** Additional stack to add some space between left (icon), value and right (icon) */} {valueLeft} @@ -147,17 +149,7 @@ const ActionInfo = ({ {...(Array.isArray(loading) && { width: loading[0], height: loading[1] })} > - {loading ? ( - typeof loading === 'string' ? ( - loading - ) : ( - MOCK_SKELETON - ) - ) : error ? ( - - ) : ( - value - )} + {loading ? (typeof loading === 'string' ? loading : MOCK_SKELETON) : value} @@ -165,6 +157,12 @@ const ActionInfo = ({ + {error && ( + + + + )} + {copyValue && ( diff --git a/packages/curve-ui-kit/src/shared/ui/WithSkeleton.tsx b/packages/curve-ui-kit/src/shared/ui/WithSkeleton.tsx index 9c34c7d8ad..8bf3d861b6 100644 --- a/packages/curve-ui-kit/src/shared/ui/WithSkeleton.tsx +++ b/packages/curve-ui-kit/src/shared/ui/WithSkeleton.tsx @@ -1,38 +1,18 @@ -import React from 'react' +import React, { ReactNode } from 'react' import { default as Skeleton, SkeletonProps } from '@mui/material/Skeleton' +import { WithWrapper } from '@ui-kit/shared/ui/WithWrapper' -/** - * A component that wraps children with a Skeleton when loading. - * Useful for when you want to use the same child for dimension inference. - * - * @example - * ```tsx - * // Basic usage - * - * Content to show when loaded - * - * - * // With metrics - * - * - * - * - * // With custom skeleton props - * - * Content to show when loaded - * - * ``` - */ type WithSkeletonProps = { /** Whether to show the skeleton or the children */ loading: boolean /** Content to render when not loading, also used for skeleton dimension inference */ - children: React.ReactNode + children: ReactNode } & SkeletonProps -export const WithSkeleton = ({ loading, children, ...skeletonProps }: WithSkeletonProps) => - loading ? {children} : <>{children} +/** + * A component that wraps children with a Skeleton when loading. + * Useful for when you want to use the same child for dimension inference. + */ +export const WithSkeleton = ({ loading, ...skeletonProps }: WithSkeletonProps) => ( + +) diff --git a/packages/curve-ui-kit/src/shared/ui/WithTooltip.tsx b/packages/curve-ui-kit/src/shared/ui/WithTooltip.tsx new file mode 100644 index 0000000000..8a6a53c4b3 --- /dev/null +++ b/packages/curve-ui-kit/src/shared/ui/WithTooltip.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { default as Tooltip, TooltipProps } from '@mui/material/Tooltip' +import { WithWrapper } from '@ui-kit/shared/ui/WithWrapper' + +/** + * A component that wraps children with a Tooltip when a title is provided. + * Useful for when you want to use the same child and don't know whether a tooltip is needed. + * The `title` passed to the Tooltip component is used to determine whether the tooltip should be rendered. + */ +export const WithTooltip = (props: TooltipProps) => diff --git a/packages/curve-ui-kit/src/shared/ui/WithWrapper.tsx b/packages/curve-ui-kit/src/shared/ui/WithWrapper.tsx new file mode 100644 index 0000000000..0dd321aec2 --- /dev/null +++ b/packages/curve-ui-kit/src/shared/ui/WithWrapper.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react' + +type WithWrapperProps = Props & { + /** Whether wrapper should be applied */ + wrap: boolean + /** Component to use as a wrapper */ + Wrapper: (props: Props & { children: ReactNode }) => ReactNode + /** Children to be wrapped */ + children: ReactNode +} + +/** + * A component that wraps children with a given Wrapper component when `wrap` is true. + * Useful for conditionally applying a wrapper component based on a boolean prop. + */ +export const WithWrapper = ({ wrap, Wrapper, children, ...wrapperProps }: WithWrapperProps) => + wrap ? {children} : children