diff --git a/components/Badge/Badge.tsx b/components/Badge/Badge.tsx index ac167f4a60..f70f0bfe68 100644 --- a/components/Badge/Badge.tsx +++ b/components/Badge/Badge.tsx @@ -1,21 +1,44 @@ -import styled from 'styled-components'; +import { FC } from 'react'; +import styled, { css } from 'styled-components'; -const Badge = styled.span<{ color: 'yellow' | 'red' | 'gray' }>` +type BadgeProps = { + color?: 'yellow' | 'red' | 'gray'; + size?: 'small' | 'regular'; + dark?: boolean; +}; + +const Badge: FC = ({ color = 'yellow', size = 'regular', dark, ...props }) => { + return ; +}; + +const BaseBadge = styled.span<{ + $color: 'yellow' | 'red' | 'gray'; + $dark?: boolean; + $size: 'small' | 'regular'; +}>` text-transform: uppercase; - padding: 1.6px 3px 1px 3px; text-align: center; - font-family: ${(props) => props.theme.fonts.black}; - color: ${(props) => props.theme.colors.selectedTheme.badge[props.color].text}; - background: ${(props) => props.theme.colors.selectedTheme.badge[props.color].background}; + ${(props) => css` + padding: 2px 6px; + padding: ${props.$size === 'small' ? '2px 4px' : '2px 6px'}; + font-size: ${props.$size === 'small' ? 10 : 12}px; + font-family: ${props.theme.fonts.black}; + color: ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].text}; + background: ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].background}; + ${props.$dark && + css` + color: ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].dark.text}; + background: ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].dark.background}; + border: 1px solid ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].dark.border}; + `} + `} border-radius: 100px; - margin-left: 5px; line-height: unset; - font-size: 10px; font-variant: all-small-caps; opacity: 1; user-select: none; + display: flex; + align-items: center; `; -Badge.displayName = 'Badge'; - export default Badge; diff --git a/components/Badge/MarketBadge.tsx b/components/Badge/MarketBadge.tsx index 08e197f1fc..ca0121924b 100644 --- a/components/Badge/MarketBadge.tsx +++ b/components/Badge/MarketBadge.tsx @@ -13,7 +13,7 @@ import Badge from './Badge'; type MarketBadgeProps = { currencyKey: FuturesMarketAsset | null; isFuturesMarketClosed: boolean; - futuresClosureReason: FuturesClosureReason; + futuresClosureReason?: FuturesClosureReason; }; type TransitionBadgeProps = { diff --git a/components/Button/Button.tsx b/components/Button/Button.tsx index f7df6df35b..d259f6cb69 100644 --- a/components/Button/Button.tsx +++ b/components/Button/Button.tsx @@ -1,5 +1,8 @@ +import { FC, ReactNode, memo } from 'react'; import styled, { css } from 'styled-components'; +import { ButtonLoader } from 'components/Loader/Loader'; + // TODO: Clean up these styles export type ButtonVariant = | 'primary' @@ -12,16 +15,17 @@ export type ButtonVariant = | 'select' | 'yellow'; -type ButtonProps = { - size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; - variant?: ButtonVariant; +type BaseButtonProps = { + $size: 'small' | 'medium' | 'large'; + $variant: ButtonVariant; isActive?: boolean; isRounded?: boolean; - mono?: boolean; fullWidth?: boolean; noOutline?: boolean; textColor?: 'yellow'; textTransform?: 'none' | 'uppercase' | 'capitalize' | 'lowercase'; + $active?: boolean; + $mono?: boolean; }; export const border = css` @@ -47,20 +51,41 @@ export const border = css` } `; -const Button = styled.button` +const sizeMap = { + small: { + paddingVertical: 8, + paddingHorizontal: 16, + height: 40, + fontSize: 13, + }, + medium: { + paddingVertical: 14, + paddingHorizontal: 26, + height: 47, + fontSize: 15, + }, + large: { + paddingVertical: 16, + paddingHorizontal: 36, + height: 55, + fontSize: 16, + }, +} as const; + +const BaseButton = styled.button` + display: flex; + justify-content: center; + align-items: center; + height: auto; cursor: pointer; position: relative; border-radius: ${(props) => (props.isRounded ? '50px' : '8px')}; - padding: 0 14px; box-sizing: border-box; - text-transform: ${(props) => props.textTransform || 'capitalize'}; + text-transform: ${(props) => props.textTransform ?? 'capitalize'}; outline: none; white-space: nowrap; - font-size: 17px; - color: ${(props) => - (props.textColor && props.theme.colors.selectedTheme.button.text[props.textColor]) || - props.theme.colors.selectedTheme.button.text.primary}; + color: ${(props) => props.theme.colors.selectedTheme.button.text[props.textColor ?? 'primary']}; transition: all 0.1s ease-in-out; ${border} &:hover { @@ -68,104 +93,64 @@ const Button = styled.button` } ${(props) => - props.variant === 'primary' && + props.$variant === 'primary' && css` background: ${props.theme.colors.selectedTheme.button.primary.background}; text-shadow: ${props.theme.colors.selectedTheme.button.primary.textShadow}; &:hover { background: ${props.theme.colors.selectedTheme.button.primary.hover}; } - `}; + `} ${(props) => - (props.noOutline || props.variant === 'flat') && + (props.noOutline || props.$variant === 'flat') && css` - background: ${(props) => props.theme.colors.selectedTheme.button.fill}; - border: ${(props) => props.theme.colors.selectedTheme.border}; + background: ${props.theme.colors.selectedTheme.button.fill}; + border: ${props.theme.colors.selectedTheme.border}; box-shadow: none; &:hover { - background: ${(props) => props.theme.colors.selectedTheme.button.fillHover}; + background: ${props.theme.colors.selectedTheme.button.fillHover}; } &::before { display: none; } - `}; + `} ${(props) => - props.variant === 'yellow' && + props.$variant === 'yellow' && css` - background: ${(props) => props.theme.colors.selectedTheme.button.yellow.fill}; - border: 1px solid ${(props) => props.theme.colors.selectedTheme.button.yellow.border}; - color: ${(props) => props.theme.colors.selectedTheme.button.yellow.text}; + background: ${props.theme.colors.selectedTheme.button.yellow.fill}; + border: 1px solid ${props.theme.colors.selectedTheme.button.yellow.border}; + color: ${props.theme.colors.selectedTheme.button.yellow.text}; box-shadow: none; &:hover { - background: ${(props) => props.theme.colors.selectedTheme.button.yellow.fillHover}; + background: ${props.theme.colors.selectedTheme.button.yellow.fillHover}; } &::before { display: none; } - `}; + `} - ${(props) => - props.mono - ? css` - font-family: ${props.theme.fonts.mono}; - ` - : css` - font-family: ${props.theme.fonts.bold}; - `}; + font-family: ${(props) => props.theme.fonts[props.$mono ? 'mono' : 'bold']}; ${(props) => - props.variant === 'secondary' && + props.$variant === 'secondary' && css` color: ${props.theme.colors.selectedTheme.button.secondary.text}; - `}; + `} ${(props) => - props.variant === 'danger' && + props.$variant === 'danger' && css` color: ${props.theme.colors.selectedTheme.red}; - `}; - - ${(props) => - props.size === 'xs' && - css` - height: 22px; - min-width: 50px; - font-size: 11px; - `}; - - ${(props) => - props.size === 'sm' && - css` - height: 41px; - min-width: 157px; - font-size: 15px; - `}; - - ${(props) => - props.size === 'md' && - css` - height: 50px; - min-width: 200px; - `}; + `} - ${(props) => - props.size === 'lg' && - css` - height: 70px; - min-width: 260px; - font-size: 19px; - `}; - - ${(props) => - props.size === 'xl' && - css` - height: 80px; - min-width: 360px; - font-size: 21px; - `}; + ${(props) => css` + height: ${sizeMap[props.$size].height}px; + padding: ${sizeMap[props.$size].paddingVertical}px ${sizeMap[props.$size].paddingHorizontal}px; + font-size: ${sizeMap[props.$size].fontSize}px; + `} ${(props) => props.fullWidth && @@ -186,4 +171,51 @@ const Button = styled.button` } `; +type ButtonProps = { + loading?: boolean; + active?: boolean; + mono?: boolean; + className?: string; + left?: ReactNode; + right?: ReactNode; + size?: 'small' | 'medium' | 'large'; + variant?: ButtonVariant; + fullWidth?: boolean; + noOutline?: boolean; + textColor?: 'yellow'; + textTransform?: 'none' | 'uppercase' | 'capitalize' | 'lowercase'; + style?: React.CSSProperties; + disabled?: boolean; + onClick?: React.MouseEventHandler | undefined; + isRounded?: boolean; +}; + +const Button: FC = memo( + ({ + loading, + children, + mono, + left, + right, + active = true, + size = 'medium', + variant = 'flat', + ...props + }) => { + return ( + + {loading ? ( + + ) : ( + <> + {left} + <>{children} + {right} + + )} + + ); + } +); + export default Button; diff --git a/components/Button/TabButton.tsx b/components/Button/TabButton.tsx index d73b8a5096..88e3dfa8e9 100644 --- a/components/Button/TabButton.tsx +++ b/components/Button/TabButton.tsx @@ -1,12 +1,14 @@ import React, { ReactNode } from 'react'; import styled, { css } from 'styled-components'; +import { Body } from 'components/Text'; + import Button from './Button'; export type TabButtonProps = { title: string; detail?: string; - badge?: number; + badgeCount?: number; icon?: any; active?: boolean; titleIcon?: ReactNode; @@ -19,111 +21,107 @@ export type TabButtonProps = { }; const TabButton: React.FC = React.memo( - ({ title, detail, badge, icon, titleIcon, ...props }) => ( - + ({ title, detail, badgeCount, icon, titleIcon, vertical, nofill, ...props }) => ( + {!!icon &&
{icon}
}
{titleIcon} -

{title}

- {!!badge &&
{badge}
} + + {title} + + {!!badgeCount &&
{badgeCount}
}
- {detail &&

{detail}

} + {detail && ( + + {detail} + + )}
) ); -const StyledButton = styled(Button)<{ - active?: boolean; - vertical?: boolean; - nofill?: boolean; +const StyledButton = styled(Button).attrs({ size: 'small' })<{ + $vertical?: boolean; + $nofill?: boolean; }>` height: initial; display: flex; align-items: center; - border-radius: ${(props) => (props.isRounded ? '100px' : '8px')}; padding-top: 10px; padding-bottom: 10px; justify-content: center; - background-color: ${(props) => - props.active - ? props.theme.colors.selectedTheme.tab.background.active - : props.theme.colors.selectedTheme.tab.background.inactive}; + p { - margin: 0; - font-size: 13px; text-align: left; } + .title-container { display: flex; flex-direction: row; align-items: center; } - .title { - text-align: center; - color: ${(props) => - props.active - ? props.theme.colors.selectedTheme.button.text.primary - : props.theme.colors.selectedTheme.gray}; - } - .detail { - color: ${(props) => - props.active ? props.theme.colors.selectedTheme.gold : props.theme.colors.selectedTheme.gray}; - margin-top: 4px; - font-size: 18px; - font-family: ${(props) => props.theme.fonts.monoBold}; - } + ${(props) => css` + flex-direction: ${props.$vertical ? 'column' : 'row'}; + border-radius: ${props.isRounded ? '100px' : '8px'}; + background-color: ${props.theme.colors.selectedTheme.tab.background[ + props.active ? 'active' : 'inactive' + ]}; - .badge { - height: 16px; - width: fit-content; - min-width: 16px; - padding-left: 4px; - padding-right: 4px; - margin-left: 7px; - font-size: 13px; - color: ${(props) => props.theme.colors.selectedTheme.black}; - background-color: ${(props) => props.theme.colors.selectedTheme.button.tab.badge.background}; - border-radius: 4px; - } + .title { + text-align: center; + color: ${props.active + ? props.theme.colors.selectedTheme.button.text.primary + : props.theme.colors.selectedTheme.gray}; + } - svg { - margin-right: ${(props) => (props.vertical ? '0' : '7px')}; - path { - ${(props) => - css` - ${props.nofill ? 'stroke' : 'fill'}: ${props.active - ? props.theme.colors.selectedTheme.button.text.primary - : props.theme.colors.selectedTheme.gray}; - `} + .detail { + color: ${props.theme.colors.selectedTheme[props.active ? 'gold' : 'gray']}; + margin-top: 4px; + font-size: 18px; } - } - &:disabled { - background-color: transparent; - p { - color: ${(props) => props.theme.colors.selectedTheme.button.tab.disabled.text}; + .badge { + height: 16px; + width: fit-content; + min-width: 16px; + padding-left: 4px; + padding-right: 4px; + margin-left: 7px; + font-size: 13px; + color: ${props.theme.colors.selectedTheme.black}; + background-color: ${props.theme.colors.selectedTheme.button.tab.badge.background}; + border-radius: 4px; } + svg { + margin-right: ${props.$vertical ? '0' : '7px'}; path { - fill: ${(props) => props.theme.colors.selectedTheme.button.tab.disabled.text}; + ${props.$nofill ? 'stroke' : 'fill'}: ${props.active + ? props.theme.colors.selectedTheme.button.text.primary + : props.theme.colors.selectedTheme.gray}; } } - .badge { - display: none; - } - } + &:disabled { + background-color: transparent; + p { + color: ${props.theme.colors.selectedTheme.button.tab.disabled.text}; + } + svg { + path { + fill: ${props.theme.colors.selectedTheme.button.tab.disabled.text}; + } + } - ${(props) => - props.vertical && - css` - display: flex; - flex-direction: ${props.vertical ? 'column' : 'row'}; - align-items: center; - `} + .badge { + display: none; + } + } + `} `; + export default TabButton; diff --git a/components/Currency/CurrencyPrice/CurrencyPrice.tsx b/components/Currency/CurrencyPrice/CurrencyPrice.tsx index 0f89a2a8a7..589361a39f 100644 --- a/components/Currency/CurrencyPrice/CurrencyPrice.tsx +++ b/components/Currency/CurrencyPrice/CurrencyPrice.tsx @@ -1,24 +1,22 @@ -import Wei, { wei } from '@synthetixio/wei'; -import { ethers } from 'ethers'; +import { wei, WeiSource } from '@synthetixio/wei'; import React, { FC, memo } from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import ChangePercent from 'components/ChangePercent'; import { ContainerRowMixin } from 'components/layout/grid'; import { CurrencyKey } from 'constants/currency'; import { formatCurrency, FormatCurrencyOptions } from 'utils/formatters/number'; -type WeiSource = Wei | number | string | ethers.BigNumber; - type CurrencyPriceProps = { currencyKey: CurrencyKey; showCurrencyKey?: boolean; price: WeiSource; sign?: string; change?: number; - conversionRate?: WeiSource | null; + conversionRate?: WeiSource; formatOptions?: FormatCurrencyOptions; truncate?: boolean; + side?: 'positive' | 'negative'; }; export const CurrencyPrice: FC = memo( @@ -30,6 +28,7 @@ export const CurrencyPrice: FC = memo( conversionRate, showCurrencyKey, formatOptions, + side, truncate = false, ...rest }) => { @@ -41,7 +40,7 @@ export const CurrencyPrice: FC = memo( } return ( - + {formatCurrency( currencyKey, @@ -59,10 +58,15 @@ export const CurrencyPrice: FC = memo( } ); -const Container = styled.span` +const Container = styled.span<{ $side?: 'positive' | 'negative' }>` ${ContainerRowMixin}; font-family: ${(props) => props.theme.fonts.mono}; color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; + ${(props) => + !!props.$side && + css` + color: ${props.theme.colors.selectedTheme.newTheme.text.number[props.$side]}; + `} `; export default CurrencyPrice; diff --git a/components/ErrorView/ErrorView.tsx b/components/ErrorView/ErrorView.tsx index ca63c5b710..fe911d2180 100644 --- a/components/ErrorView/ErrorView.tsx +++ b/components/ErrorView/ErrorView.tsx @@ -45,7 +45,7 @@ export const ErrorView: FC = memo( {retryButton && ( <> - diff --git a/components/InfoBox/InfoBox.tsx b/components/InfoBox/InfoBox.tsx index ffb3944ba3..a182c92618 100644 --- a/components/InfoBox/InfoBox.tsx +++ b/components/InfoBox/InfoBox.tsx @@ -1,121 +1,75 @@ -import { memo, FC, useState, useCallback } from 'react'; +import { memo, FC } from 'react'; import styled, { css } from 'styled-components'; import CaretDownIcon from 'assets/svg/app/caret-down-gray.svg'; -import * as Text from 'components/Text'; +import { Body } from 'components/Text'; import { NO_VALUE } from 'constants/placeholder'; -export type DetailedInfo = { - value: string | React.ReactNode; +type InfoBoxRowProps = { + title: string; + value: React.ReactNode; keyNode?: React.ReactNode; valueNode?: React.ReactNode; - color?: 'green' | 'red' | 'gold' | undefined; spaceBeneath?: boolean; compactBox?: boolean; - expandable?: boolean; - subItems?: Record; -}; - -type InfoBoxProps = { - details: Record; - style?: React.CSSProperties; - className?: string; + color?: 'green' | 'red' | 'gold' | undefined; disabled?: boolean; dataTestId?: string; -}; - -const InfoBox: FC = memo(({ details, disabled, dataTestId, ...props }) => { - const [expandedRows, setExpandedRows] = useState>(new Set()); - - const onToggleExpand = useCallback( - (key: string) => { - expandedRows.has(key) ? expandedRows.delete(key) : expandedRows.add(key); - setExpandedRows(new Set([...expandedRows])); - }, - [expandedRows] - ); - - return ( - - {Object.entries(details).map(([key, value], index) => ( - <> - - {value?.subItems && expandedRows.has(key) - ? Object.entries(value.subItems).map(([key, value], index) => ( - - )) - : null} - - ))} - - ); -}); - -type InfoBoxValueProps = { - title: string; - value?: DetailedInfo | null; - disabled?: boolean; - dataTestId: string; expandable?: boolean; expanded?: boolean; - isSubtItem?: boolean; + isSubItem?: boolean; onToggleExpand?: (key: string) => void; }; -const InfoBoxValue: FC = memo( - ({ title, value, disabled, dataTestId, expandable, expanded, isSubtItem, onToggleExpand }) => { - if (!value) return null; - - return ( - <> - {value.compactBox ? ( - value.keyNode - ) : ( - onToggleExpand?.(title) : undefined} +export const InfoBoxRow: FC = memo( + ({ + title, + value, + keyNode, + compactBox, + disabled, + dataTestId, + expandable, + expanded, + isSubItem, + onToggleExpand, + children, + color, + valueNode, + spaceBeneath, + }) => ( + <> + {compactBox ? ( + keyNode + ) : ( + onToggleExpand?.(title) : undefined} + > + + {title}: {keyNode} {expandable ? expanded ? : : null} + + - - {title}: {value.keyNode}{' '} - {expandable ? expanded ? : : null} - - - {disabled ? NO_VALUE : value.value} - {value.valueNode} - - - )} - {value?.spaceBeneath &&
} - - ); - } + {disabled ? NO_VALUE : value} + {valueNode} + +
+ )} + {spaceBeneath &&
} + {expandable && expanded && children} + + ) ); -const Row = styled.div<{ onClick?: (title: string) => void; isSubtItem?: boolean }>` +const Row = styled.div<{ $isSubItem?: boolean }>` cursor: ${(props) => (props.onClick ? 'pointer' : 'default')}; - padding-left: ${(props) => (props.isSubtItem ? '10px' : '0')}; - border-left: ${(props) => (props.isSubtItem ? props.theme.colors.selectedTheme.border : '0')}; + padding-left: ${(props) => (props.$isSubItem ? '10px' : '0')}; + border-left: ${(props) => (props.$isSubItem ? props.theme.colors.selectedTheme.border : '0')}; border-width: 2px; display: flex; justify-content: space-between; @@ -125,7 +79,7 @@ const Row = styled.div<{ onClick?: (title: string) => void; isSubtItem?: boolean } `; -const InfoBoxContainer = styled.div` +export const InfoBoxContainer = styled.div` border: ${(props) => props.theme.colors.selectedTheme.border}; border-radius: 10px; padding: 14px; @@ -133,23 +87,18 @@ const InfoBoxContainer = styled.div` width: 100%; `; -const InfoBoxKey = styled(Text.Body)` +const InfoBoxKey = styled(Body)` color: ${(props) => props.theme.colors.selectedTheme.text.label}; - font-size: 13px; text-transform: capitalize; `; -const ValueText = styled(Text.Body)<{ +const ValueText = styled(Body).attrs({ mono: true })<{ $disabled?: boolean; - $color?: DetailedInfo['color']; - isSubtItem?: boolean; + $color?: InfoBoxRowProps['color']; + $isSubItem?: boolean; }>` - color: ${(props) => - props.isSubtItem - ? props.theme.colors.selectedTheme.text.label - : props.theme.colors.selectedTheme.text.value}; - font-family: ${(props) => props.theme.fonts.mono}; - font-size: 13px; + color: ${(props) => props.theme.colors.selectedTheme.text[props.$isSubItem ? 'label' : 'value']}; + cursor: default; ${(props) => props.$color === 'red' && @@ -184,5 +133,3 @@ const HideIcon = styled(ExpandIcon)` transform: rotate(180deg); margin-bottom: -4px; `; - -export default InfoBox; diff --git a/components/InfoBox/index.ts b/components/InfoBox/index.ts index 60640b6a0c..0b7f721929 100644 --- a/components/InfoBox/index.ts +++ b/components/InfoBox/index.ts @@ -1 +1 @@ -export { default } from './InfoBox'; +export { InfoBoxContainer, InfoBoxRow } from './InfoBox'; diff --git a/components/Input/CustomInput.tsx b/components/Input/CustomInput.tsx deleted file mode 100644 index 87b10043dd..0000000000 --- a/components/Input/CustomInput.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { memo, FC, useCallback } from 'react'; -import styled from 'styled-components'; - -type CustomInputProps = { - placeholder?: string; - value?: string | number; - onChange: (e: React.ChangeEvent, value: string) => void; - right: React.ReactNode; - left?: React.ReactNode; - style?: React.CSSProperties; - className?: string; - disabled?: boolean; - id?: string; - defaultValue?: any; - dataTestId?: string; - textAlign?: string; - invalid?: boolean; -}; - -const INVALID_CHARS = ['-', '+', 'e']; - -const CustomInput: FC = memo( - ({ - value, - placeholder, - onChange, - right, - left, - disabled, - id, - defaultValue, - dataTestId, - textAlign = 'left', - ...props - }) => { - const handleChange = useCallback( - (e: React.ChangeEvent) => { - const standardizedNum = e.target.value.replace(/,/g, '.').replace(/[e+-]/gi, ''); - if (isNaN(Number(standardizedNum))) return; - onChange(e, standardizedNum); - }, - [onChange] - ); - - return ( - - {typeof left === 'string' ? {left} : left} - { - if (INVALID_CHARS.includes(e.key)) { - e.preventDefault(); - } - }} - id={id} - defaultValue={defaultValue} - /> - {typeof right === 'string' ? {right} : right} - - ); - } -); - -const CustomInputContainer = styled.div<{ textAlign: string; invalid?: boolean }>` - display: flex; - align-items: center; - justify-content: space-between; - box-sizing: border-box; - height: 46px; - background: ${(props) => props.theme.colors.selectedTheme.input.secondary.background}; - box-shadow: ${(props) => props.theme.colors.selectedTheme.input.shadow}; - border: ${(props) => props.theme.colors.selectedTheme.border}; - border-color: ${(props) => - props.invalid ? props.theme.colors.selectedTheme.red : props.theme.colors.selectedTheme.border}; - - border-radius: 10px; - padding: 0 10px; - - input { - display: flex; - flex: 1; - margin-right: 4px; - font-family: ${(props) => props.theme.fonts.mono}; - font-size: 18px; - line-height: 22px; - background-color: transparent; - border: none; - text-align: ${(props) => props.textAlign || 'left'}; - color: ${(props) => - props.invalid - ? props.theme.colors.selectedTheme.red - : props.theme.colors.selectedTheme.button.text.primary}; - width: 100%; - - &:focus { - outline: none; - } - - ::placeholder { - color: ${(props) => props.theme.colors.selectedTheme.input.placeholder}; - } - } - - span { - font-family: ${(props) => props.theme.fonts.mono}; - font-size: 16px; - color: ${(props) => props.theme.colors.selectedTheme.input.placeholder}; - } -`; - -export default CustomInput; diff --git a/components/Input/CustomNumericInput.tsx b/components/Input/CustomNumericInput.tsx deleted file mode 100644 index d1018a0301..0000000000 --- a/components/Input/CustomNumericInput.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { ChangeEvent, FC, memo } from 'react'; -import styled from 'styled-components'; - -import Input from './Input'; - -type CustomNumericInputProps = { - value: string; - placeholder?: string; - suffix: string; - onChange: (e: ChangeEvent, value: string) => void; - className?: string; - defaultValue?: any; - maxValue?: number; - disabled?: boolean; - id?: string; -}; - -const CustomNumericInput: FC = memo( - ({ - value, - placeholder, - suffix, - onChange, - className, - defaultValue, - maxValue, - disabled, - id, - ...rest - }) => { - const handleOnChange = (e: ChangeEvent) => { - const { value } = e.target; - const standardizedNum = value - .replace(/[^0-9.,]/g, '') - .replace(/,/g, '.') - .substring(0, 4); - if (isNaN(Number(standardizedNum))) return; - const max = maxValue || 0; - const valueIsAboveMax = max !== 0 && Number(standardizedNum) > max; - if (!valueIsAboveMax) { - onChange(e, standardizedNum); - } - }; - - return ( - - - - ); - } -); - -export const InputWrapper = styled.div<{ $length: number; $suffix: string }>` - position: relative; - overflow: hidden; - ::after { - position: absolute; - top: calc(25%); - left: calc(${(props) => props.$length} * 1ch + 1.3ch)); - content: var(${(props) => (props.$length === 0 ? '' : props.$suffix)}); - font-family: ${(props) => props.theme.fonts.mono}; - font-size: 18px; - color: ${(props) => props.theme.colors.selectedTheme.input.placeholder}; - } -`; - -export const StyledInput = styled(Input)` - font-family: ${(props) => props.theme.fonts.mono}; - text-overflow: ellipsis; -`; - -export default CustomNumericInput; diff --git a/components/Input/InputBalanceLabel.tsx b/components/Input/InputBalanceLabel.tsx index 7acbccb5a0..2bed200b54 100644 --- a/components/Input/InputBalanceLabel.tsx +++ b/components/Input/InputBalanceLabel.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { FlexDivRowCentered } from 'components/layout/flex'; +import { Body } from 'components/Text'; import { formatCurrency } from 'utils/formatters/number'; type Props = { @@ -34,12 +35,9 @@ export default function InputBalanceLabel({ balance, currencyKey, onSetAmount }: export const BalanceContainer = styled(FlexDivRowCentered)` margin-bottom: 8px; - p { - margin: 0; - } `; -export const BalanceText = styled.p` +export const BalanceText = styled(Body)` color: ${(props) => props.theme.colors.selectedTheme.gray}; span { color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; diff --git a/components/Input/NumericInput.tsx b/components/Input/NumericInput.tsx index 6444b64167..a679e5d17e 100644 --- a/components/Input/NumericInput.tsx +++ b/components/Input/NumericInput.tsx @@ -1,55 +1,162 @@ -import React, { ChangeEvent, FC, memo, useCallback } from 'react'; +import { FC, memo, useCallback } from 'react'; import styled, { css } from 'styled-components'; -import Input from './Input'; +import Spacer from 'components/Spacer'; -type NumericInputProps = Omit, 'onChange'> & { - value: string | number; - placeholder?: string; - onChange: (e: ChangeEvent, value: string) => void; - className?: string; - defaultValue?: any; - disabled?: boolean; - id?: string; +type NumericInputProps = Omit< + React.InputHTMLAttributes, + 'onChange' | 'maxLength' +> & { + value: string; + onChange: (e: React.ChangeEvent, value: string) => void; + left?: React.ReactNode; + right?: React.ReactNode; + dataTestId?: string; + invalid?: boolean; bold?: boolean; + textAlign?: string; + suffix?: string; + max?: number; + maxLength?: number | 'none'; }; const INVALID_CHARS = ['-', '+', 'e']; -const NumericInput: FC = memo(({ onChange, bold, ...props }) => { - const handleOnChange = useCallback( - (e: ChangeEvent) => { - const standardizedNum = e.target.value.replace(/,/g, '.').replace(/[e+-]/gi, ''); - if (isNaN(Number(standardizedNum))) return; - onChange(e, standardizedNum); - }, - [onChange] - ); - - return ( - { - if (INVALID_CHARS.includes(e.key)) { - e.preventDefault(); +const isInvalid = (key: string) => INVALID_CHARS.includes(key); + +const NumericInput: FC = memo( + ({ + value, + onChange, + left, + right, + dataTestId, + invalid, + bold, + textAlign, + max = 0, + maxLength = 'none', + className, + ...props + }) => { + const handleChange = useCallback( + (e: React.ChangeEvent) => { + let standardizedNum = e.target.value.replace(/[^0-9.,]/g, '').replace(/,/g, '.'); + + if (maxLength !== 'none') { + standardizedNum = standardizedNum.substring(0, maxLength); + } + // TODO: make regex only accept valid numbers, so we don't need to check again. + if (isNaN(Number(standardizedNum))) return; + const valueIsAboveMax = max !== 0 && Number(standardizedNum) > max; + if (!valueIsAboveMax) { + onChange(e, standardizedNum); } - }} - $bold={bold} - {...props} - /> - ); -}); - -export const StyledInput = styled(Input)<{ $bold?: boolean }>` - font-family: ${(props) => props.theme.fonts.mono}; + }, + [onChange, max, maxLength] + ); + + return ( + + {left && ( + <> + {left} + + + )} + { + if (isInvalid(e.key)) { + e.preventDefault(); + } + }} + {...props} + /> + {right && ( + <> + + {right} + + )} + + ); + } +); + +const InputContainer = styled.div<{ + $invalid?: boolean; + $bold?: boolean; + $textAlign?: string; + $suffix?: string; + $length: number; +}>` + display: flex; + align-items: center; + justify-content: space-between; + background: ${(props) => props.theme.colors.selectedTheme.input.secondary.background}; + box-shadow: ${(props) => props.theme.colors.selectedTheme.input.shadow}; + border: ${(props) => props.theme.colors.selectedTheme.border}; + border-radius: 10px; + padding: 0 10px; + height: 46px; + box-sizing: border-box; + + & > input { + display: flex; + flex: 1; + font-family: ${(props) => (props.$bold ? props.theme.fonts.monoBold : props.theme.fonts.mono)}; + font-size: 18px; + line-height: 22px; + padding: 0; + background-color: transparent; + border: none; + text-overflow: ellipsis; + min-width: 0px; + width: 100%; + color: ${(props) => + props.$invalid + ? props.theme.colors.selectedTheme.red + : props.theme.colors.selectedTheme.button.text.primary}; + + ${(props) => + props.$textAlign && + css` + text-align: ${props.$textAlign}; + `} + + &:focus { + outline: none; + } + + ::placeholder { + color: ${(props) => props.theme.colors.selectedTheme.input.placeholder}; + } + } + ${(props) => - props.$bold && + props.$suffix && css` - font-family: ${props.theme.fonts.monoBold}; - `} - text-overflow: ellipsis; + ::after { + position: absolute; + top: calc(25%); + left: calc(${props.$length} * 1ch + 1.3ch)); + content: var(${props.$length === 0 ? '""' : props.$suffix}); + font-family: ${props.theme.fonts.mono}; + font-size: 18px; + color: ${props.theme.colors.selectedTheme.input.placeholder}; + } + `} `; export default NumericInput; diff --git a/components/Input/SearchInput.tsx b/components/Input/SearchInput.tsx deleted file mode 100644 index aae17f061e..0000000000 --- a/components/Input/SearchInput.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import styled from 'styled-components'; - -import Input from './Input'; - -export const TextInput = styled(Input).attrs({ type: 'search' })``; - -export default TextInput; diff --git a/components/Pill.tsx b/components/Pill.tsx new file mode 100644 index 0000000000..a2549490dc --- /dev/null +++ b/components/Pill.tsx @@ -0,0 +1,46 @@ +import { FC, memo } from 'react'; +import styled, { css } from 'styled-components'; + +type PillProps = React.ButtonHTMLAttributes & { + size?: 'small' | 'large'; + color?: 'yellow' | 'gray' | 'red'; + outline?: boolean; +}; + +const Pill: FC = memo(({ size = 'small', color = 'yellow', outline, ...props }) => { + return ; +}); + +const BasePill = styled.button<{ + $size: 'small' | 'large'; + $color: 'yellow' | 'gray' | 'red'; + $outline?: boolean; +}>` + ${(props) => css` + padding: ${props.$size === 'small' ? '5px' : '8px'}; + font-size: ${props.$size === 'small' ? 10 : 12}px; + font-family: ${props.theme.fonts.black}; + background: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].background}; + color: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].text}; + border: 1px solid ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].border}; + border-radius: 50px; + cursor: pointer; + + ${props.$outline && + css` + background: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].outline + .background}; + color: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].outline.text}; + border: 1px solid + ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].outline.border}; + `} + + &:hover { + background: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].hover.background}; + color: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].hover.text}; + border: 1px solid ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].hover.border}; + } + `} +`; + +export default Pill; diff --git a/components/README.md b/components/README.md index 90bfef15f2..73496f99a1 100644 --- a/components/README.md +++ b/components/README.md @@ -1,3 +1,33 @@ # Kwenta Components +This folder contains all the base components that are used across the Kwenta interface. We try to ensure that they remain in sync will our latest designs. Components here have a number of props that cover every use case that is consistent with our design system. + +Most of the components in this folder are viewable in our [Storybook](). + ## Folder Structure + +We try to ensure that folders are as flat as (reasonbly) possible. For this, reason, most individual components can be found under `{component-name}.tsx`. However in the future, some domain specific components may be found in subfolders like `mobile`, `futures` or `exchange`. The components are only expected to be used in the context of the folders in which they are contained. + +## Checklist + +This list tracks features needed to complete the component refactor, as well as any component-related issues noticed with the current UI. This will help us get closer to a standard UI. + +- [ ] Create primary, secondary and tertiary colors for both body text and headings. Make sure that designs conform to them and add them to the theme file. +- [ ] Create component that takes in a wei value and returns the number with either a red, green or neutral color, depending on whether the value is positive, negative, zero or overridden. +- [x] Make the `InfoBox` (and similar components) more composable, rather than prop-driven, to ensure that render performance does not suffer. +- [ ] Complete Button component: Align completely with designs and figure out way to get rid of standalone `TabButton` component +- [ ] Decide whether or not it makes sense to completely decouple futures components from data requirements to make them Storybook compatible +- [ ] Finally crack down on styled components in the frontend. We should reduce the number of styled components by at least 60% to consider the refactor a success. +- [ ] Enforce uniform spacing between UI components. Consider adding a margin prop to most UI components (will also help get rid of most styled components). +- [ ] Make theme files contain everything that pertains to component variants and their different styles. +- [ ] Upgrade `react-table` to `@tanstack/react-table`. It's more recent and has better typings. We'll have to do a little bit of work though, to deal with breaking changes between the versions and make sure we improve on the render performance of the `Table` component. +- [ ] Finish implementing the `Text.Display` component. +- [ ] Do a pass through of all text-related components in the codebase, and make sure they use the new components. Also, add new props for use cases that aren't covered with the current implementation. +- [ ] Consider making `value` prop on `NumericValue` optional. +- [ ] Completely restructure the `ProfitCalculator` component. +- [ ] Add reusable `Label` component under `Text`. It should also support tooltip descriptions. + +## Guidelines + +- [ ] Avoid using `Styled` as a prefix to the names of styled components. Instead, prefer names that describe the function of the component. This makes it easier to understand the component's function at first glance. It also makes it easier to refactor or replace the component when necessary. +- [ ] Avoid using absolute positioning unless necessary. This helps avoid z-index conflicts and makes it easier for components to be adapted to multiple screen sizes. diff --git a/components/SegmentedControl/SegmentedControl.tsx b/components/SegmentedControl/SegmentedControl.tsx index 8e261a9926..fa5f5f9820 100644 --- a/components/SegmentedControl/SegmentedControl.tsx +++ b/components/SegmentedControl/SegmentedControl.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FC, memo } from 'react'; import styled from 'styled-components'; type StyleType = 'tab' | 'check' | 'button'; @@ -13,31 +13,26 @@ interface SegmentedControlProps { onChange(index: number): void; } -function SegmentedControl({ - values, - selectedIndex, - suffix, - onChange, - styleType = 'tab', - ...props -}: SegmentedControlProps) { - return ( - - {values.map((value, index) => ( - onChange(index)} - > - {styleType === 'check' && } - {value} - {suffix} - - ))} - - ); -} +const SegmentedControl: FC = memo( + ({ values, selectedIndex, suffix, onChange, styleType = 'tab', ...props }) => { + return ( + + {values.map((value, index) => ( + onChange(index)} + > + {styleType === 'check' && } + {value} + {suffix} + + ))} + + ); + } +); const SegmentedControlContainer = styled.div<{ $length: number; styleType: StyleType }>` ${(props) => diff --git a/components/Select/Select.tsx b/components/Select/Select.tsx index 833c87641b..9629a3fdd9 100644 --- a/components/Select/Select.tsx +++ b/components/Select/Select.tsx @@ -14,7 +14,7 @@ export const DropdownIndicator = (props: any) => { ); }; -const StyledCaretDownIcon = styled(CaretDownIcon)` +export const StyledCaretDownIcon = styled(CaretDownIcon)` width: 11px; color: ${(props) => props.theme.colors.selectedTheme.gray}; `; diff --git a/components/StakeCard/StakeCard.tsx b/components/StakeCard/StakeCard.tsx index 6aad7e75a3..b3bee1f9fe 100644 --- a/components/StakeCard/StakeCard.tsx +++ b/components/StakeCard/StakeCard.tsx @@ -124,7 +124,7 @@ const StakeCard: FC = memo( -