Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ const LoanInfoParameters = ({ llamma, llammaId }: Props) => {

return (
<Box grid gridRowGap="1">
<DetailInfo label={t`Band width factor`} size="md">
<DetailInfo label={t`Band width factor`}>
<span>{formatNumber(llamma?.A, { useGrouping: false })}</span>
</DetailInfo>
<DetailInfo label={t`Base price`} size="md">
<DetailInfo label={t`Base price`}>
{typeof loanDetails?.basePrice !== 'undefined' && (
<Chip
size="md"
Expand All @@ -29,7 +29,7 @@ const LoanInfoParameters = ({ llamma, llammaId }: Props) => {
</Chip>
)}
</DetailInfo>
<DetailInfo label={t`Oracle price`} size="md">
<DetailInfo label={t`Oracle price`}>
{typeof priceInfo?.oraclePrice !== 'undefined' && (
<Chip
size="md"
Expand All @@ -40,7 +40,7 @@ const LoanInfoParameters = ({ llamma, llammaId }: Props) => {
</Chip>
)}
</DetailInfo>
<DetailInfo label={t`Borrow rate`} size="md">
<DetailInfo label={t`Borrow rate`}>
{typeof parameters?.rate !== 'undefined' && (
<Chip
size="md"
Expand Down
49 changes: 29 additions & 20 deletions packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -16,69 +16,75 @@ 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<StackProps, 'sx' | 'className'> & {
/** 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 */
valueLeft?: ReactNode
/** 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 */
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
/** Whether the component is in a loading state. Can be boolean or string (string value is used for skeleton width inference) */
loading?: boolean | string
size?: ActionInfoSize
/** 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 */
testId?: string
sx?: SxProps
}

const labelSize = {
small: 'bodyXsRegular',
medium: 'bodyMRegular',
large: 'bodyMRegular',
} as const satisfies Record<ComponentSize, TypographyVariantKey>
} as const satisfies Record<ActionInfoSize, TypographyVariantKey>

const prevValueSize = {
small: 'bodySRegular',
medium: 'bodyMRegular',
large: 'bodyMRegular',
} as const satisfies Record<ComponentSize, TypographyVariantKey>
} as const satisfies Record<ActionInfoSize, TypographyVariantKey>

const valueSize = {
small: 'bodyXsBold',
medium: 'highlightM',
large: 'headingSBold',
} as const satisfies Record<ComponentSize, TypographyVariantKey>
} as const satisfies Record<ActionInfoSize, TypographyVariantKey>

const ActionInfo = ({
label,
Expand All @@ -98,18 +104,18 @@ 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)
openSnackbar()
}, [copyValue, openSnackbar])

return (
<Stack direction="row" alignItems="center" gap={Spacing.sm} sx={sx} data-testid={testId}>
<Stack direction="row" alignItems="center" gap={Spacing.sm} data-testid={testId} {...styleProps}>
<Typography flexGrow={1} variant={labelSize[size]} color={labelColor ?? 'textSecondary'} textAlign="start">
{label}
</Typography>
Expand Down Expand Up @@ -140,7 +146,10 @@ const ActionInfo = ({
<Stack direction="row" alignItems="center" gap={Spacing.xxs} data-testid={`${testId}-value`}>
{valueLeft}

<WithSkeleton loading={!!loading}>
<WithSkeleton
loading={!!loading}
{...(Array.isArray(loading) && { width: loading[0], height: loading[1] })}
>
<Typography variant={valueSize[size]} color={error ? 'error' : (valueColor ?? 'textPrimary')}>
{loading ? (
typeof loading === 'string' ? (
Expand Down
130 changes: 75 additions & 55 deletions packages/ui/src/DetailInfo/DetailInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,99 @@
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

type Variant = 'error' | 'warning' | 'success' | ''
type Size = 'xs' | 'sm' | 'md' | 'lg'

/**
* 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',
warning: 'warning',
success: 'success',
} satisfies Record<Variant, ActionInfoProps['valueColor']>

type Props = {
children: ReactNode
action?: ReactNode
className?: string
isBold?: boolean | null
isDivider?: boolean
isMultiLine?: boolean
label?: ReactNode
loading?: boolean
loadingSkeleton?: [number, number]
size?: Size
textLeft?: boolean
tooltip?: ReactNode
variant?: Variant
}

const DetailInfo = ({
action: Action,
const OldDetailInfo = ({
className,
isBold,
isDivider,
label,
loading,
loadingSkeleton,
tooltip: Tooltip,
tooltip,
variant,
children,
size = 'sm',
...props
}: Props) => {
const classNames = `${className} ${isDivider ? 'divider' : ''}`

return (
<Wrapper
{...props}
size={size}
className={classNames}
grid
gridAutoFlow="column"
gridColumnGap={2}
isDivider={isDivider}
fillWidth
>
{label && <DetailLabel>{label}</DetailLabel>}
<DetailValue haveLabel={!!label} isBold={isBold} variant={variant}>
{loading && <Loader skeleton={loadingSkeleton} />}
{!loading && (
<>
{children || '-'} {!!Tooltip && Tooltip} {Action && Action}
</>
)}
</DetailValue>
</Wrapper>
)
}
}: Props) => (
<Wrapper
className={`${className} ${isDivider ? 'divider' : ''}`}
grid
gridAutoFlow="column"
gridColumnGap={2}
isDivider={isDivider}
fillWidth
>
{label && <DetailLabel>{label}</DetailLabel>}
<DetailValue haveLabel={!!label} isBold={isBold} variant={variant}>
{loading && <Loader skeleton={loadingSkeleton} />}
{!loading && (
<>
{children || '-'} {!!tooltip && tooltip}
</>
)}
</DetailValue>
</Wrapper>
)

const NewDetailInfo = ({
className,
isBold,
isDivider,
label,
loading,
loadingSkeleton,
tooltip,
variant,
children,
}: Props) => (
<>
{isDivider && <Divider sx={{ marginBlock: Spacing.sm }} />}
<ActionInfo
className={className}
label={label}
value={children || '-'}
valueColor={VariantToColorMap[variant || '']}
valueTooltip={tooltip}
copy={typeof children === 'string'}
error={variant === 'error'}
loading={loading && (loadingSkeleton || true)}
copyValue={typeof children === 'string' ? children : ''}
size={isBold ? 'large' : 'medium'}
/>
</>
)

export const DetailLabel = styled.span`
display: inline-block;
Expand Down Expand Up @@ -94,19 +127,12 @@ const DetailValue = styled.div<DetailValeProps>`
}};
`

interface WrapperProps extends Pick<Props, 'isDivider' | 'isMultiLine' | 'size' | 'textLeft'> {}
interface WrapperProps extends Pick<Props, 'isDivider' | 'isMultiLine'> {}

const Wrapper = styled(Box)<WrapperProps>`
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);`
}
}}
font-size: var(--font-size-3);

.svg-tooltip {
margin-top: 0.25rem;
Expand Down Expand Up @@ -136,20 +162,14 @@ const Wrapper = styled(Box)<WrapperProps>`
`
}
}}
${DetailValue} {
${({ textLeft }) => {
if (textLeft) {
return `
justify-content: flex-start;
text-align: left;
`
}
}}
}

.svg-tooltip {
top: 0.2rem;
}
`

export default DetailInfo
export default function DetailInfo(props: Props) {
const [releaseChannel] = useReleaseChannel()
const DetailInfo = releaseChannel === ReleaseChannel.Beta ? NewDetailInfo : OldDetailInfo
return <DetailInfo {...props} />
}