diff --git a/.github/actions/setup-env/action.yml b/.github/actions/setup-env/action.yml index b27d0df44..ad8dfd48d 100644 --- a/.github/actions/setup-env/action.yml +++ b/.github/actions/setup-env/action.yml @@ -32,7 +32,9 @@ runs: run: | sudo apt-get update sudo apt-get -y install python3-pip python3-dev - pip install awscli --upgrade --user + python -m venv venv + source venv/bin/activate + pip install awscli --upgrade - name: Project dependencies setup, node version ${{ inputs.node-version }} shell: bash run: yarn install --frozen-lockfile diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index efc38fb54..98f1dd6e0 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -23,7 +23,7 @@ jobs: # branch should not be protected branch: 'main' # user names of users allowed to contribute without CLA - allowlist: rmeissner,germartinez,Uxio0,dasanra,francovenica,luarx,DaniSomoza,yagopv,JagoFigueroa,jmealy,usame-algan,bot* + allowlist: clovisdasilvaneto,rmeissner,germartinez,Uxio0,dasanra,francovenica,luarx,DaniSomoza,yagopv,JagoFigueroa,jmealy,usame-algan,bot* # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken # enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) diff --git a/apps/tx-builder/package.json b/apps/tx-builder/package.json index 4f59aedf6..57fca853c 100644 --- a/apps/tx-builder/package.json +++ b/apps/tx-builder/package.json @@ -9,7 +9,7 @@ "@material-ui/icons": "^4.11.3", "@material-ui/lab": "^4.0.0-alpha.60", "@safe-global/safe-apps-provider": "^0.18.0", - "@safe-global/safe-deployments": "^1.26.0", + "@safe-global/safe-deployments": "^1.27.0", "@safe-global/safe-gateway-typescript-sdk": "^3.19.0", "axios": "^1.6.0", "evm-proxy-detection": "1.0.0", diff --git a/apps/tx-builder/src/assets/add-new-batch.svg b/apps/tx-builder/src/assets/add-new-batch.svg deleted file mode 100644 index 5303c416d..000000000 --- a/apps/tx-builder/src/assets/add-new-batch.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/apps/tx-builder/src/assets/arrowtotheblock.svg b/apps/tx-builder/src/assets/arrowtotheblock.svg new file mode 100644 index 000000000..54436b814 --- /dev/null +++ b/apps/tx-builder/src/assets/arrowtotheblock.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/tx-builder/src/assets/empty-library-dark.svg b/apps/tx-builder/src/assets/empty-library-dark.svg new file mode 100644 index 000000000..487d38fd7 --- /dev/null +++ b/apps/tx-builder/src/assets/empty-library-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/tx-builder/src/assets/empty-library-light.svg b/apps/tx-builder/src/assets/empty-library-light.svg new file mode 100644 index 000000000..649f8dc7a --- /dev/null +++ b/apps/tx-builder/src/assets/empty-library-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/tx-builder/src/assets/empty-library.svg b/apps/tx-builder/src/assets/empty-library.svg deleted file mode 100644 index 7e6198d63..000000000 --- a/apps/tx-builder/src/assets/empty-library.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/tx-builder/src/assets/fonts/DMSans700.woff2 b/apps/tx-builder/src/assets/fonts/DMSans700.woff2 new file mode 100644 index 000000000..4b0a5ded0 Binary files /dev/null and b/apps/tx-builder/src/assets/fonts/DMSans700.woff2 differ diff --git a/apps/tx-builder/src/assets/fonts/DMSansRegular.woff2 b/apps/tx-builder/src/assets/fonts/DMSansRegular.woff2 new file mode 100644 index 000000000..21f9cbc2b Binary files /dev/null and b/apps/tx-builder/src/assets/fonts/DMSansRegular.woff2 differ diff --git a/apps/tx-builder/src/assets/new-batch-dark.svg b/apps/tx-builder/src/assets/new-batch-dark.svg new file mode 100644 index 000000000..f80dc18a4 --- /dev/null +++ b/apps/tx-builder/src/assets/new-batch-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/tx-builder/src/assets/new-batch-light.svg b/apps/tx-builder/src/assets/new-batch-light.svg new file mode 100644 index 000000000..82b946d2d --- /dev/null +++ b/apps/tx-builder/src/assets/new-batch-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/tx-builder/src/components/Accordion/index.tsx b/apps/tx-builder/src/components/Accordion/index.tsx new file mode 100644 index 000000000..36fc14311 --- /dev/null +++ b/apps/tx-builder/src/components/Accordion/index.tsx @@ -0,0 +1,87 @@ +import { ReactElement } from 'react' +import AccordionMUI, { AccordionProps as AccordionMUIProps } from '@material-ui/core/Accordion' +import AccordionSummaryMUI, { + AccordionSummaryProps as AccordionSummaryMUIProps, +} from '@material-ui/core/AccordionSummary' +import styled from 'styled-components' +import FixedIcon from '../FixedIcon' + +type AccordionProps = AccordionMUIProps & { + compact?: boolean +} + +type StyledAccordionProps = AccordionMUIProps & { + $compact?: AccordionProps['compact'] +} + +const StyledAccordion = styled(AccordionMUI)` + &.MuiAccordion-root { + border-radius: ${({ $compact }) => ($compact ? '8px' : '0')}; + border: ${({ $compact, theme }) => ($compact ? '2px solid ' + theme.palette.divider : 'none')}; + border-bottom: 2px solid ${({ theme }) => theme.palette.divider}; + margin-bottom: ${({ $compact }) => ($compact ? '16px' : '0')}; + overflow: hidden; + + &:before { + height: 0; + } + + &:first-child { + border-top: 2px solid ${({ theme }) => theme.palette.divider}; + } + + &.Mui-expanded { + margin: ${({ $compact }) => ($compact ? '0 0 16px 0' : '0')}; + } + + .MuiAccordionDetails-root { + padding: 16px; + } + } +` + +const StyledAccordionSummary = styled(AccordionSummaryMUI)` + &.MuiAccordionSummary-root { + &.Mui-expanded { + min-height: 48px; + border-bottom: 2px solid ${({ theme }) => theme.palette.divider}; + background-color: ${({ theme }) => theme.palette.background.default}; + } + + &:hover { + background-color: ${({ theme }) => theme.palette.background.default}; + } + + .MuiAccordionSummary-content { + &.Mui-expanded { + margin: 0; + } + } + .MuiIconButton-root { + font-size: 0; + padding: 16px; + } + } +` + +export const Accordion = ({ compact, children, ...props }: AccordionProps): ReactElement => { + return ( + + {children} + + ) +} + +export const AccordionSummary = ({ + children, + ...props +}: AccordionSummaryMUIProps): ReactElement => { + return ( + } {...props}> + {children} + + ) +} + +export { default as AccordionActions } from '@material-ui/core/AccordionActions' +export { default as AccordionDetails } from '@material-ui/core/AccordionDetails' diff --git a/apps/tx-builder/src/components/Button.tsx b/apps/tx-builder/src/components/Button.tsx new file mode 100644 index 000000000..3f1edba72 --- /dev/null +++ b/apps/tx-builder/src/components/Button.tsx @@ -0,0 +1,208 @@ +import React, { ReactElement, ReactNode, HTMLAttributes } from 'react' +import ButtonMUI, { ButtonProps as ButtonMUIProps } from '@material-ui/core/Button' +import { alpha } from '@material-ui/core/styles' + +import styled, { css, DefaultTheme, FlattenInterpolation, ThemeProps } from 'styled-components' +import { Icon, IconProps } from './Icon' + +type Colors = 'primary' | 'secondary' | 'error' +type Variations = 'bordered' | 'contained' | 'outlined' + +type CustomButtonMuiProps = Omit & { + to?: string + component?: ReactNode +} +type LocalProps = { + children?: ReactNode + color?: Colors + variant?: Variations + iconType?: IconProps['type'] + iconSize?: IconProps['size'] +} + +type Props = LocalProps & CustomButtonMuiProps & HTMLAttributes + +const StyledIcon = styled(Icon)` + margin-right: 5px; +` + +const customStyles: { + [key in Colors]: { + [key in Variations]: FlattenInterpolation> + } +} = { + primary: { + contained: css` + color: ${({ theme }) => theme.palette.background.main}; + background-color: ${({ theme }) => theme.palette.primary.main}; + box-shadow: 1px 2px 10px ${alpha('#28363D', 0.18)}; + + &:hover { + color: ${({ theme }) => theme.palette.background.main}; + background-color: ${({ theme }) => theme.palette.primary.dark}; + } + `, + outlined: css` + color: ${({ theme }) => theme.palette.primary.main}; + background-color: transparent; + path.icon-color { + fill: ${({ theme }) => theme.palette.primary.main}; + } + + &.Mui-disabled { + color: ${({ theme }) => theme.palette.primary.main}; + } + + &:hover { + color: ${({ theme }) => theme.palette.primary.dark}; + } + `, + bordered: css` + color: ${({ theme }) => theme.palette.primary.main}; + background-color: transparent; + border: 2px solid ${({ theme }) => theme.palette.primary.main}; + path.icon-color { + fill: ${({ theme }) => theme.palette.primary.main}; + } + + &.Mui-disabled { + color: ${({ theme }) => theme.palette.primary.main}; + } + + &:hover { + background: ${({ theme }) => theme.palette.background.light}; + } + `, + }, + secondary: { + contained: css` + color: ${({ theme }) => theme.palette.primary}; + background-color: ${({ theme }) => theme.palette.secondary.main}; + box-shadow: 1px 2px 10px ${alpha('#28363D', 0.18)}; + + path.icon-color { + color: ${({ theme }) => theme.palette.common.primary}; + } + + &:hover { + path.icon-color { + color: ${({ theme }) => theme.palette.common.primary}; + } + + background-color: ${({ theme }) => theme.palette.secondary.dark}; + } + `, + outlined: css` + color: ${({ theme }) => theme.palette.secondary.main}; + background-color: transparent; + path.icon-color { + fill: ${({ theme }) => theme.palette.secondary.main}; + } + + &.Mui-disabled { + color: ${({ theme }) => theme.palette.secondary.main}; + } + `, + bordered: css` + color: ${({ theme }) => theme.palette.secondary.main}; + background-color: transparent; + border: 2px solid ${({ theme }) => theme.palette.secondary.main}; + path.icon-color { + fill: ${({ theme }) => theme.palette.secondary.main}; + } + + &.Mui-disabled { + color: ${({ theme }) => theme.palette.secondary.main}; + } + `, + }, + error: { + contained: css` + color: ${({ theme }) => theme.palette.error.main}; + background-color: ${({ theme }) => theme.palette.error.background}; + + &:hover { + background-color: ${({ theme }) => theme.palette.error.light}; + color: ${({ theme }) => theme.palette.error.dark}; + } + `, + outlined: css` + color: ${({ theme }) => theme.palette.error.main}; + background-color: transparent; + path.icon-color { + fill: ${({ theme }) => theme.palette.error.main}; + } + + &.Mui-disabled { + color: ${({ theme }) => theme.palette.error.main}; + } + `, + bordered: css` + color: ${({ theme }) => theme.palette.error.main}; + background-color: transparent; + border: 2px solid ${({ theme }) => theme.palette.error.main}; + path.icon-color { + fill: ${({ theme }) => theme.palette.error.main}; + } + + &.Mui-disabled { + color: ${({ theme }) => theme.palette.error.main}; + } + `, + }, +} + +const StyledButton = styled(ButtonMUI)<{ $localProps: LocalProps }>` + && { + font-weight: 700; + padding: 8px 1.4rem; + min-width: 120px; + + &.MuiButton-root { + text-transform: none; + border-radius: 8px; + letter-spacing: 0; + } + + &.Mui-disabled { + color: ${({ theme }) => theme.palette.background.main}; + } + + path.icon-color { + fill: ${({ theme }) => theme.palette.background.main}; + } + + &:disabled { + opacity: 0.5; + } + + ${({ $localProps }) => { + if ($localProps.color !== undefined && $localProps.variant !== undefined) { + return customStyles[$localProps.color][$localProps.variant] + } + }} + } +` + +const Button = ({ + children, + color = 'primary', + variant = 'contained', + iconType, + iconSize, + // We need destructuring all LocalProps, remaining props are for CustomButtonMuiProps + ...buttonMuiProps +}: Props): ReactElement => { + return ( + + {iconType && iconSize && } + {children} + + ) +} + +export default Button diff --git a/apps/tx-builder/src/components/Card/index.tsx b/apps/tx-builder/src/components/Card/index.tsx new file mode 100644 index 000000000..83f161ce1 --- /dev/null +++ b/apps/tx-builder/src/components/Card/index.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import styled from 'styled-components' +import { alpha } from '@material-ui/core/styles' + +const StyledCard = styled.div` + box-shadow: 1px 2px 10px 0 ${alpha('#28363D', 0.18)}; + border-radius: 8px; + padding: 24px; + background-color: ${({ theme }) => theme.palette.common.white}; + position: relative; +` + +const Disabled = styled.div` + opacity: 0.5; + position: absolute; + height: 100%; + width: 100%; + background-color: ${({ theme }) => theme.palette.common.white}; + z-index: 1; + top: 0; + left: 0; +` + +type Props = { + className?: string + disabled?: boolean +} & React.HTMLAttributes + +const Card: React.FC = ({ className, children, disabled, ...rest }): React.ReactElement => ( + + {disabled && } + {children} + +) + +export default Card diff --git a/apps/tx-builder/src/components/CreateNewBatchCard.tsx b/apps/tx-builder/src/components/CreateNewBatchCard.tsx index 3ffe0e30e..e1ae18f63 100644 --- a/apps/tx-builder/src/components/CreateNewBatchCard.tsx +++ b/apps/tx-builder/src/components/CreateNewBatchCard.tsx @@ -1,13 +1,18 @@ -import { useRef } from 'react' -import { ButtonLink, Icon, Text } from '@gnosis.pm/safe-react-components' +import { useContext, useRef } from 'react' import { alpha } from '@material-ui/core' import Hidden from '@material-ui/core/Hidden' import styled from 'styled-components' import { useTheme } from '@material-ui/core/styles' -import { ReactComponent as CreateNewBatchSVG } from '../assets/add-new-batch.svg' +import { ReactComponent as CreateNewBatchLightSvg } from '../assets/new-batch-light.svg' +import { ReactComponent as CreateNewBatchDarkSvg } from '../assets/new-batch-dark.svg' +import { ReactComponent as ArrowBlock } from '../assets/arrowtotheblock.svg' import useDropZone from '../hooks/useDropZone' import { useMediaQuery } from '@material-ui/core' +import { Icon } from './Icon' +import Text from './Text' +import ButtonLink from './buttons/ButtonLink' +import { EModes, ThemeModeContext } from '../theme/SafeThemeProvider' type CreateNewBatchCardProps = { onFileSelected: (file: File | null) => void @@ -15,6 +20,7 @@ type CreateNewBatchCardProps = { const CreateNewBatchCard = ({ onFileSelected }: CreateNewBatchCardProps) => { const theme = useTheme() + const mode = useContext(ThemeModeContext) const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')) const fileRef = useRef(null) @@ -38,36 +44,42 @@ const CreateNewBatchCard = ({ onFileSelected }: CreateNewBatchCardProps) => { return ( - + {mode === EModes.DARK ? : } + - - {isAcceptError ? ( - - The uploaded file is not a valid JSON file - - ) : ( - <> - - Drag and drop a JSON file or - - choose a file - - - )} - - + + + Start creating a new batch + or + + {isAcceptError ? ( + + The uploaded file is not a valid JSON file + + ) : ( + <> + + Drag and drop a JSON file or + + choose a file + + + )} + + + ) } @@ -75,26 +87,48 @@ const CreateNewBatchCard = ({ onFileSelected }: CreateNewBatchCardProps) => { export default CreateNewBatchCard const Wrapper = styled.div<{ isSmallScreen: boolean }>` + text-align: center; + position: relative; margin-top: ${({ isSmallScreen }) => (isSmallScreen ? '0' : '64px')}; ` +const StyledArrowBlock = styled(ArrowBlock)` + position: absolute; + left: -2px; + top: 7rem; +` + +const StyledCreateBatchContent = styled.div` + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; +` + const StyledDragAndDropFileContainer = styled.div<{ dragOver: Boolean fullWidth: boolean error: Boolean }>` box-sizing: border-box; - max-width: ${({ fullWidth }) => (fullWidth ? '100%' : '420px')}; - border: 2px dashed ${({ theme, error }) => (error ? theme.colors.error : '#008c73')}; + max-width: ${({ fullWidth }) => (fullWidth ? '100%' : '430px')}; + width: 100%; + border: 2px dashed + ${({ theme, error }) => (error ? theme.palette.error.main : theme.palette.secondary.dark)}; border-radius: 8px; - background-color: ${({ theme, error }) => (error ? alpha(theme.colors.error, 0.7) : '#eaf7f4')}; + background-color: ${({ theme, error }) => + error ? alpha(theme.palette.error.main, 0.7) : theme.palette.secondary.background}; padding: 24px; - margin: 24px auto 0 auto; + margin: 6px auto; display: flex; justify-content: center; align-items: center; + svg { + margin-right: 4px; + } + ${({ dragOver, error, theme }) => { if (dragOver) { return ` @@ -104,22 +138,31 @@ const StyledDragAndDropFileContainer = styled.div<{ } return ` - border-color: ${error ? theme.colors.error : '#008c73'}; - background-color: ${error ? alpha(theme.colors.error, 0.7) : '#eaf7f4'}; + border-color: ${error ? theme.palette.error.main : theme.palette.secondary.dark}; + background-color: ${ + error ? alpha(theme.palette.error.main, 0.7) : theme.palette.secondary.background + }; ` }} ` const StyledText = styled(Text)<{ error?: Boolean }>` - margin-left: 4px; - color: ${({ error }) => (error ? '#FFF' : '#566976')}; + && { + color: ${({ error, theme }) => + error ? theme.palette.common.white : theme.palette.text.secondary}; + } ` const StyledButtonLink = styled(ButtonLink)` + margin-left: 0.3rem; padding: 0; - text-decoration: none; && > p { - font-size: 16px; + color: ${({ theme }) => theme.palette.upload.primary}; + text-decoration: underline; + + &:hover { + color: ${({ theme }) => theme.palette.backdrop.main}; + } } ` diff --git a/apps/tx-builder/src/components/Divider.tsx b/apps/tx-builder/src/components/Divider.tsx new file mode 100644 index 000000000..e423550a1 --- /dev/null +++ b/apps/tx-builder/src/components/Divider.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import styled from 'styled-components' + +type Props = { + className?: string + orientation?: 'vertical' | 'horizontal' +} + +const HorizontalDivider = styled.div` + margin: 16px -1.6rem; + border-top: solid 1px ${({ theme }) => theme.palette.border.light}; + width: calc(100% + 3.2rem); +` + +const VerticalDivider = styled.div` + border-right: 1px solid ${({ theme }) => theme.legacy.colors.separator}; + margin: 0 5px; + height: 100%; +` + +const Divider = ({ className, orientation }: Props): React.ReactElement => { + return orientation === 'vertical' ? ( + + ) : ( + + ) +} + +export default Divider diff --git a/apps/tx-builder/src/components/Dot/index.tsx b/apps/tx-builder/src/components/Dot/index.tsx new file mode 100644 index 000000000..df53a5643 --- /dev/null +++ b/apps/tx-builder/src/components/Dot/index.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import styled from 'styled-components' +import { type Theme } from '@material-ui/core/styles' + +type Props = { + className?: string + color: keyof Theme['palette'] +} + +const StyledDot = styled.div` + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + height: 36px; + width: 36px; + background-color: ${({ theme, color }) => theme.palette[color].main}; +` + +const Dot: React.FC = ({ children, ...rest }): React.ReactElement => ( + {children} +) + +export default Dot diff --git a/apps/tx-builder/src/components/ETHHashInfo.tsx b/apps/tx-builder/src/components/ETHHashInfo.tsx new file mode 100644 index 000000000..e03611de0 --- /dev/null +++ b/apps/tx-builder/src/components/ETHHashInfo.tsx @@ -0,0 +1,149 @@ +import React, { useState } from 'react' +import styled from 'styled-components' + +import { BreakpointDefaults } from '@material-ui/core/styles/createBreakpoints' +import { textShortener } from '../utils/strings' +import { EllipsisMenuItem } from '@gnosis.pm/safe-react-components' +import Text from './Text' +import { Theme } from '@material-ui/core' +import ExplorerButton from './buttons/ExplorerButton' +import Identicon, { identiconSizes } from './buttons/Identicon' +import CopyToClipboardBtn from './buttons/CopyToClipboardBtn' +import EllipsisMenu from './EllipsisMenu' + +export type ExplorerInfo = () => { url: string; alt: string } + +const StyledContainer = styled.div` + display: flex; + align-items: center; +` + +const AvatarContainer = styled.div` + display: flex; + margin-right: 8px; +` + +const InfoContainer = styled.div` + display: flex; + align-items: flex-start; + justify-content: center; + flex-direction: column; +` + +const AddressContainer = styled.div` + display: flex; + align-items: center; + gap: 4px; +` + +const StyledImg = styled.img<{ size: keyof typeof identiconSizes }>` + height: ${({ size }) => identiconSizes[size]}; + width: ${({ size }) => identiconSizes[size]}; +` + +type Props = { + className?: string + hash: string + showHash?: boolean + shortenHash?: number + name?: string + strongName?: boolean + textColor?: keyof Theme['palette'] + textSize?: keyof BreakpointDefaults + showAvatar?: boolean + customAvatar?: string + customAvatarFallback?: string + avatarSize?: keyof BreakpointDefaults + showCopyBtn?: boolean + menuItems?: EllipsisMenuItem[] + explorerUrl?: ExplorerInfo +} + +type ShortNameProps = + | { + shouldShowShortName: boolean + shouldCopyShortName?: boolean + shortName: string + } + | { + shouldShowShortName?: boolean + shouldCopyShortName: boolean + shortName: string + } + | { + shouldShowShortName?: never + shouldCopyShortName?: never + shortName?: string + } + +type EthHashInfoProps = Props & ShortNameProps + +const EthHashInfo = ({ + hash, + showHash = true, + name, + className, + shortenHash, + showAvatar, + customAvatar, + customAvatarFallback, + avatarSize = 'md', + showCopyBtn, + menuItems, + explorerUrl, + shortName, + shouldShowShortName, + shouldCopyShortName, +}: EthHashInfoProps): React.ReactElement => { + const [fallbackToIdenticon, setFallbackToIdenticon] = useState(false) + const [fallbackSrc, setFallabckSrc] = useState(undefined) + + const setAppImageFallback = (): void => { + if (customAvatarFallback && !fallbackToIdenticon) { + setFallabckSrc(customAvatarFallback) + } else { + setFallbackToIdenticon(true) + } + } + + return ( + + {showAvatar && ( + + {!fallbackToIdenticon && customAvatar ? ( + + ) : ( + + )} + + )} + + + {name && {name}} + + {showHash && ( + + {shouldShowShortName && ( + + {shortName}: + + )} + {shortenHash ? textShortener(hash, shortenHash + 2, shortenHash) : hash} + + )} + {showCopyBtn && ( + + )} + {explorerUrl && } + {menuItems && } + + + + ) +} + +export default EthHashInfo diff --git a/apps/tx-builder/src/components/EllipsisMenu/index.tsx b/apps/tx-builder/src/components/EllipsisMenu/index.tsx new file mode 100644 index 000000000..ff563b512 --- /dev/null +++ b/apps/tx-builder/src/components/EllipsisMenu/index.tsx @@ -0,0 +1,102 @@ +import { ClickAwayListener } from '@material-ui/core' +import Menu from '@material-ui/core/Menu' +import MenuItem from '@material-ui/core/MenuItem' +import React from 'react' +import styled from 'styled-components' +import FixedIcon from '../FixedIcon' + +const StyledMenu = styled(Menu)` + && { + .MuiMenu-paper { + box-shadow: 0 0 4px rgba(0, 0, 0, 0.1); + } + + .MuiMenu-list { + div:not(:first-child) { + border-top: 1px solid ${({ theme }) => theme.palette.divider}; + } + } + } +` + +const MenuWrapper = styled.div` + display: flex; +` + +const MenuItemWrapper = styled.div` + :focus { + outline-color: ${({ theme }) => theme.palette.divider}; + } +` + +const IconWrapper = styled.button` + background: none; + border: none; + cursor: pointer; + margin: 0; + border-radius: 50%; + transition: background-color 0.2s ease-in-out; + outline-color: transparent; + height: 24px; + width: 24px; + + span { + display: flex; + } + + :hover { + background-color: ${({ theme }) => theme.palette.divider}; + } +` + +export type EllipsisMenuItem = { + label: string + disabled?: boolean + onClick: () => void +} + +type Props = { + menuItems: EllipsisMenuItem[] +} + +const EllipsisMenu = ({ menuItems }: Props): React.ReactElement => { + const [anchorEl, setAnchorEl] = React.useState(null) + + const handleClick = (event: React.MouseEvent): void => + setAnchorEl(event.currentTarget) + + const closeMenuHandler = () => { + setAnchorEl(null) + } + + const onMenuItemClick = (item: EllipsisMenuItem) => { + item.onClick() + closeMenuHandler() + } + + return ( + + + + + + + {menuItems.map(item => ( + + onMenuItemClick(item)}> + {item.label} + + + ))} + + + + ) +} + +export default EllipsisMenu diff --git a/apps/tx-builder/src/components/FixedIcon/images/arrowReceived.tsx b/apps/tx-builder/src/components/FixedIcon/images/arrowReceived.tsx new file mode 100644 index 000000000..20ab6167d --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/arrowReceived.tsx @@ -0,0 +1,12 @@ +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/arrowReceivedWhite.tsx b/apps/tx-builder/src/components/FixedIcon/images/arrowReceivedWhite.tsx new file mode 100644 index 000000000..07d980afd --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/arrowReceivedWhite.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/arrowSent.tsx b/apps/tx-builder/src/components/FixedIcon/images/arrowSent.tsx new file mode 100644 index 000000000..9fa0ddf79 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/arrowSent.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/arrowSentWhite.tsx b/apps/tx-builder/src/components/FixedIcon/images/arrowSentWhite.tsx new file mode 100644 index 000000000..3818a64a5 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/arrowSentWhite.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/arrowSort.tsx b/apps/tx-builder/src/components/FixedIcon/images/arrowSort.tsx new file mode 100644 index 000000000..598716081 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/arrowSort.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/bullit.tsx b/apps/tx-builder/src/components/FixedIcon/images/bullit.tsx new file mode 100644 index 000000000..3e9569207 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/bullit.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/chevronDown.tsx b/apps/tx-builder/src/components/FixedIcon/images/chevronDown.tsx new file mode 100644 index 000000000..9e9aaa309 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/chevronDown.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/chevronLeft.tsx b/apps/tx-builder/src/components/FixedIcon/images/chevronLeft.tsx new file mode 100644 index 000000000..fcd0c42e7 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/chevronLeft.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/chevronRight.tsx b/apps/tx-builder/src/components/FixedIcon/images/chevronRight.tsx new file mode 100644 index 000000000..42209a6dd --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/chevronRight.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/chevronUp.tsx b/apps/tx-builder/src/components/FixedIcon/images/chevronUp.tsx new file mode 100644 index 000000000..5d6bdc440 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/chevronUp.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/connectedRinkeby.tsx b/apps/tx-builder/src/components/FixedIcon/images/connectedRinkeby.tsx new file mode 100644 index 000000000..98a397a7a --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/connectedRinkeby.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/connectedWallet.tsx b/apps/tx-builder/src/components/FixedIcon/images/connectedWallet.tsx new file mode 100644 index 000000000..53bd24a1b --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/connectedWallet.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/creatingInProgress.tsx b/apps/tx-builder/src/components/FixedIcon/images/creatingInProgress.tsx new file mode 100644 index 000000000..542b2c4ed --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/creatingInProgress.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +const icon = ( + + + + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/dropdownArrowSmall.tsx b/apps/tx-builder/src/components/FixedIcon/images/dropdownArrowSmall.tsx new file mode 100644 index 000000000..0bc2c5d75 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/dropdownArrowSmall.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/networkError.tsx b/apps/tx-builder/src/components/FixedIcon/images/networkError.tsx new file mode 100644 index 000000000..daff9c9e3 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/networkError.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +const icon = ( + + + + + + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/notConnected.tsx b/apps/tx-builder/src/components/FixedIcon/images/notConnected.tsx new file mode 100644 index 000000000..1b2b19246 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/notConnected.tsx @@ -0,0 +1,18 @@ +import React from 'react' + +const icon = ( + + + + + + + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/notOwner.tsx b/apps/tx-builder/src/components/FixedIcon/images/notOwner.tsx new file mode 100644 index 000000000..e2ea0945d --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/notOwner.tsx @@ -0,0 +1,34 @@ +import React from 'react' + +const icon = ( + + + + + + + + + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/options.tsx b/apps/tx-builder/src/components/FixedIcon/images/options.tsx new file mode 100644 index 000000000..f8689a2ff --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/options.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +const icon = ( + + + + + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/plus.tsx b/apps/tx-builder/src/components/FixedIcon/images/plus.tsx new file mode 100644 index 000000000..526b2fb64 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/plus.tsx @@ -0,0 +1,20 @@ +const icon = ( + + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/settingsChange.tsx b/apps/tx-builder/src/components/FixedIcon/images/settingsChange.tsx new file mode 100644 index 000000000..bec1264e9 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/settingsChange.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const icon = ( + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/images/threeDots.tsx b/apps/tx-builder/src/components/FixedIcon/images/threeDots.tsx new file mode 100644 index 000000000..7a8eda5e9 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/images/threeDots.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +const icon = ( + + + + + + + +) + +export default icon diff --git a/apps/tx-builder/src/components/FixedIcon/index.tsx b/apps/tx-builder/src/components/FixedIcon/index.tsx new file mode 100644 index 000000000..65c33bba6 --- /dev/null +++ b/apps/tx-builder/src/components/FixedIcon/index.tsx @@ -0,0 +1,74 @@ +import React from 'react' + +import arrowSort from './images/arrowSort' +import connectedRinkeby from './images/connectedRinkeby' +import connectedWallet from './images/connectedWallet' +import bullit from './images/bullit' +import dropdownArrowSmall from './images/dropdownArrowSmall' +import arrowReceived from './images/arrowReceived' +import arrowReceivedWhite from './images/arrowReceivedWhite' +import arrowSent from './images/arrowSent' +import arrowSentWhite from './images/arrowSentWhite' +import threeDots from './images/threeDots' +import options from './images/options' +import plus from './images/plus' +import chevronRight from './images/chevronRight' +import chevronLeft from './images/chevronLeft' +import chevronUp from './images/chevronUp' +import chevronDown from './images/chevronDown' +import settingsChange from './images/settingsChange' +import creatingInProgress from './images/creatingInProgress' +import notOwner from './images/notOwner' +import notConnected from './images/notConnected' +import networkError from './images/networkError' +import styled from 'styled-components' + +const StyledIcon = styled.span` + .icon-color { + fill: ${({ theme }) => theme.palette.text.primary}; + } + + .icon-stroke { + fill: ${({ theme }) => theme.palette.text.primary}; + } +` +const icons = { + arrowSort, + connectedRinkeby, + connectedWallet, + bullit, + dropdownArrowSmall, + arrowReceived, + arrowReceivedWhite, + arrowSent, + arrowSentWhite, + threeDots, + options, + plus, + chevronRight, + chevronLeft, + chevronUp, + chevronDown, + settingsChange, + creatingInProgress, + notOwner, + notConnected, + networkError, +} + +export type IconType = typeof icons +export type IconTypes = keyof IconType + +type Props = { + type: IconTypes + className?: string +} + +/** + * The `FixedIcon` renders an icon + */ +function FixedIcon({ type, className }: Props): React.ReactElement { + return {icons[type]} +} + +export default FixedIcon diff --git a/apps/tx-builder/src/components/GenericModal.tsx b/apps/tx-builder/src/components/GenericModal.tsx new file mode 100644 index 000000000..365838180 --- /dev/null +++ b/apps/tx-builder/src/components/GenericModal.tsx @@ -0,0 +1,124 @@ +import React from 'react' +import Modal from '@material-ui/core/Modal' +import { makeStyles } from '@material-ui/core/styles' +import { alpha } from '@material-ui/core/styles' +import styled from 'styled-components' +import Media from 'react-media' +import { Typography } from '@material-ui/core' +import { Icon } from './Icon' + +const StyledButton = styled.button` + background: none; + border: none; + padding: 5px; + width: 26px; + height: 26px; + + span { + margin-right: 0; + } + + :focus { + outline: none; + } + + :hover { + background: ${({ theme }) => theme.palette.divider}; + border-radius: 16px; + } +` + +const TitleSection = styled.div` + display: flex; + justify-content: space-between; + padding: 16px 24px; + border-bottom: 2px solid ${({ theme }) => theme.palette.divider}; +` + +const BodySection = styled.div<{ + withoutBodyPadding?: boolean + smallHeight: boolean +}>` + max-height: ${({ smallHeight }) => (smallHeight ? '280px' : '460px')}; + overflow-y: auto; + padding: ${({ withoutBodyPadding }) => (withoutBodyPadding ? '0' : '16px 24px')}; +` + +const FooterSection = styled.div` + border-top: 2px solid ${({ theme }) => theme.palette.divider}; + padding: 16px 24px; +` + +const ModalPaper = styled.div` + background: ${({ theme }) => theme.palette.background.paper}; + color: ${({ theme }) => theme.palette.text.primary}; +` + +export type GenericModalProps = { + title: string | React.ReactNode + body: React.ReactNode + withoutBodyPadding?: boolean + footer?: React.ReactNode + onClose: () => void +} + +const useStyles = makeStyles({ + modal: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + overflowY: 'scroll', + background: alpha('#E8E7E6', 0.75), + }, + + paper: { + position: (props: { smallHeight: boolean }) => (props.smallHeight ? 'relative' : 'absolute'), + top: (props: { smallHeight: boolean }) => (props.smallHeight ? 'unset' : '121px'), + minWidth: '500px', + width: (props: { smallHeight: boolean }) => (props.smallHeight ? '500px' : 'inherit'), + borderRadius: '8px', + boxShadow: `0 0 0.75 0 #28363D`, + + '&:focus': { + outline: 'none', + }, + }, +}) + +const GenericModalComponent = ({ + body, + footer, + onClose, + title, + withoutBodyPadding, + smallHeight, +}: GenericModalProps & { smallHeight: boolean }) => { + const classes = useStyles({ smallHeight }) + + return ( + + + + {title} + + + + + + + {body} + + + {footer && {footer}} + + + ) +} + +const GenericModal = (props: GenericModalProps): React.ReactElement => ( + + {matches => } + +) + +export default GenericModal diff --git a/apps/tx-builder/src/components/Header.tsx b/apps/tx-builder/src/components/Header.tsx index e4cb36827..23a55f8e7 100644 --- a/apps/tx-builder/src/components/Header.tsx +++ b/apps/tx-builder/src/components/Header.tsx @@ -1,4 +1,3 @@ -import { FixedIcon, Icon, Text, Title, Tooltip } from '@gnosis.pm/safe-react-components' import { Link, useLocation, useNavigate } from 'react-router-dom' import styled from 'styled-components' @@ -12,6 +11,11 @@ import { import { useTransactionLibrary } from '../store' import ChecksumWarning from './ChecksumWarning' import ErrorAlert from './ErrorAlert' +import { Tooltip } from './Tooltip' +import { Icon } from './Icon' +import FixedIcon from './FixedIcon' +import { Typography } from '@material-ui/core' +import Text from './Text' const HELP_ARTICLE_LINK = 'https://help.safe.global/en/articles/40841-transaction-builder' @@ -51,31 +55,25 @@ const Header = () => { {showTitle ? ( <> {/* Transaction Builder Title */} - Transaction Builder - - + Transaction Builder + + - + ) : ( {/* Go Back link */} - {goBackLabel[previousUrl]} + {goBackLabel[previousUrl]} )} {showLinkToLibrary && ( - {`(${batches.length}) Your transaction library`} + {`(${batches.length}) Your transaction library`} @@ -94,29 +92,44 @@ const HeaderWrapper = styled.header` width: 100%; display: flex; align-items: center; - border-bottom: 1px solid #e2e3e3; + border-bottom: 1px solid ${({ theme }) => theme.palette.border.light}; z-index: 10; - background-color: white; + background-color: ${({ theme }) => theme.palette.background.paper}; + color: ${({ theme }) => theme.palette.text.primary}; height: 70px; padding: 0 40px; box-sizing: border-box; ` -const StyledTitle = styled(Title)` - font-size: 20px; - margin: 0 10px 0 0; +const StyledTitle = styled(Typography)` + && { + font-size: 20px; + font-weight: 700; + margin: 0 10px 0 0; + } ` const StyledLink = styled(Link)` display: flex; align-items: center; - color: #000000; + color: ${({ theme }) => theme.palette.common.black}; font-size: 16px; text-decoration: none; + + > span { + padding-top: 3px; + + path { + fill: ${({ theme }) => theme.palette.common.black}; + } + } ` const StyledLeftLinkLabel = styled(Text)` - margin-left: 8px; + && { + margin-left: 8px; + font-weight: 700; + } ` const RigthLinkWrapper = styled.div` @@ -126,5 +139,13 @@ const RigthLinkWrapper = styled.div` ` const StyledRightLinkLabel = styled(Text)` - margin-right: 8px; + && { + font-weight: 700; + margin-right: 8px; + } +` + +const StyledIconLink = styled.a` + display: flex; + align-items: center; ` diff --git a/apps/tx-builder/src/components/Icon/images/alert.tsx b/apps/tx-builder/src/components/Icon/images/alert.tsx new file mode 100644 index 000000000..b1618a408 --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/alert.tsx @@ -0,0 +1,34 @@ +const Alert = { + sm: ( + + + + + + + + ), + md: ( + + + + + + + + ), +} + +export default Alert diff --git a/apps/tx-builder/src/components/Icon/images/bookmark.tsx b/apps/tx-builder/src/components/Icon/images/bookmark.tsx new file mode 100644 index 000000000..ec17e2171 --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/bookmark.tsx @@ -0,0 +1,26 @@ +const Bookmark = { + sm: ( + + + + ), + md: ( + + + + ), +} + +export default Bookmark diff --git a/apps/tx-builder/src/components/Icon/images/bookmarkFilled.tsx b/apps/tx-builder/src/components/Icon/images/bookmarkFilled.tsx new file mode 100644 index 000000000..c0128163d --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/bookmarkFilled.tsx @@ -0,0 +1,30 @@ +const BookMarkFilled = { + sm: ( + + + + + + ), + md: ( + + + + + + ), +} + +export default BookMarkFilled diff --git a/apps/tx-builder/src/components/Icon/images/check.tsx b/apps/tx-builder/src/components/Icon/images/check.tsx new file mode 100644 index 000000000..da0c7925c --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/check.tsx @@ -0,0 +1,26 @@ +const Check = { + sm: ( + + + + + + + ), + md: ( + + + + + + + ), +} + +export default Check diff --git a/apps/tx-builder/src/components/Icon/images/code.tsx b/apps/tx-builder/src/components/Icon/images/code.tsx new file mode 100644 index 000000000..10b25bbf8 --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/code.tsx @@ -0,0 +1,27 @@ +const Code = { + sm: ( + + + + + + + ), + md: ( + + + + + + + ), +} + +export default Code diff --git a/apps/tx-builder/src/components/Icon/images/copy.tsx b/apps/tx-builder/src/components/Icon/images/copy.tsx new file mode 100644 index 000000000..652ff813e --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/copy.tsx @@ -0,0 +1,28 @@ +const Copy = { + sm: ( + + + + + + + ), + md: ( + + + + + + + ), +} + +export default Copy diff --git a/apps/tx-builder/src/components/Icon/images/cross.tsx b/apps/tx-builder/src/components/Icon/images/cross.tsx new file mode 100644 index 000000000..6c9eb6758 --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/cross.tsx @@ -0,0 +1,28 @@ +import React from 'react' + +const Cross = { + sm: ( + + + + + + + ), + md: ( + + + + + + + ), +} + +export default Cross diff --git a/apps/tx-builder/src/components/Icon/images/delete.tsx b/apps/tx-builder/src/components/Icon/images/delete.tsx new file mode 100644 index 000000000..8a60a4bbc --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/delete.tsx @@ -0,0 +1,54 @@ +const Delete = { + sm: ( + + + + + + ), + md: ( + + + + + + ), +} + +export default Delete diff --git a/apps/tx-builder/src/components/Icon/images/edit.tsx b/apps/tx-builder/src/components/Icon/images/edit.tsx new file mode 100644 index 000000000..4fb1e7329 --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/edit.tsx @@ -0,0 +1,26 @@ +const Edit = { + sm: ( + + + + ), + md: ( + + + + ), +} + +export default Edit diff --git a/apps/tx-builder/src/components/Icon/images/externalLink.tsx b/apps/tx-builder/src/components/Icon/images/externalLink.tsx new file mode 100644 index 000000000..0e80e8874 --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/externalLink.tsx @@ -0,0 +1,36 @@ +const ExternalLink = { + sm: ( + + + + + + + + ), + md: ( + + + + + + + + ), +} + +export default ExternalLink diff --git a/apps/tx-builder/src/components/Icon/images/import.tsx b/apps/tx-builder/src/components/Icon/images/import.tsx new file mode 100644 index 000000000..051dfdd5a --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/import.tsx @@ -0,0 +1,40 @@ +const Import = { + sm: ( + + + + + ), + md: ( + + + + + ), +} + +export default Import diff --git a/apps/tx-builder/src/components/Icon/images/info.tsx b/apps/tx-builder/src/components/Icon/images/info.tsx new file mode 100644 index 000000000..3c1485c86 --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/info.tsx @@ -0,0 +1,40 @@ +const Info = { + sm: ( + + + + + + + + + ), + md: ( + + + + + + + + + ), +} + +export default Info diff --git a/apps/tx-builder/src/components/Icon/images/termsOfUse.tsx b/apps/tx-builder/src/components/Icon/images/termsOfUse.tsx new file mode 100644 index 000000000..6a300b2dd --- /dev/null +++ b/apps/tx-builder/src/components/Icon/images/termsOfUse.tsx @@ -0,0 +1,26 @@ +const TermsOfUse = { + sm: ( + + + + ), + md: ( + + + + ), +} + +export default TermsOfUse diff --git a/apps/tx-builder/src/components/Icon/index.tsx b/apps/tx-builder/src/components/Icon/index.tsx new file mode 100644 index 000000000..0830ccf30 --- /dev/null +++ b/apps/tx-builder/src/components/Icon/index.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import styled from 'styled-components' + +import { type Theme } from '@material-ui/core/styles' +import { Tooltip } from '../Tooltip' + +import alert from './images/alert' +import bookmark from './images/bookmark' +import bookmarkFilled from './images/bookmarkFilled' +import check from './images/check' +import code from './images/code' +import copy from './images/copy' +import cross from './images/cross' +import deleteIcon from './images/delete' +import edit from './images/edit' +import externalLink from './images/externalLink' +import importImg from './images/import' +import info from './images/info' +import termsOfUse from './images/termsOfUse' + +const StyledIcon = styled.span<{ color?: keyof Theme['palette'] }>` + display: inline-flex; + + .icon-color { + fill: ${({ theme, color }) => (color ? theme.palette[color].main : '#B2B5B2')}; + } + + .icon-stroke { + stroke: ${({ theme, color }) => (color ? theme.palette[color].main : '#B2B5B2')}; + } +` + +const icons = { + alert, + bookmark, + bookmarkFilled, + check, + copy, + code, + cross, + delete: deleteIcon, + edit, + externalLink, + importImg, + info, + termsOfUse, +} + +export type IconType = typeof icons +export type IconTypes = keyof IconType + +export type IconProps = { + type: IconTypes + size: 'sm' | 'md' + color?: keyof Theme['palette'] + tooltip?: string + className?: string +} + +/** + * The `Icon` renders an icon, it can be one already defined specified by + * the type Iconprops or custom one using the customUrl. + */ +export const Icon = ({ type, size, color, tooltip, className }: IconProps): React.ReactElement => { + const IconElement = ( + + {icons[type][size]} + + ) + return tooltip === undefined ? ( + IconElement + ) : ( + + {IconElement} + + ) +} diff --git a/apps/tx-builder/src/components/IconText/index.tsx b/apps/tx-builder/src/components/IconText/index.tsx new file mode 100644 index 000000000..f2ec58512 --- /dev/null +++ b/apps/tx-builder/src/components/IconText/index.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import styled from 'styled-components' +import { type Theme } from '@material-ui/core/styles' + +import { Icon, IconProps, IconType } from '../Icon' +import Text from '../Text' + +const iconTextMargins = { + xxs: '4px', + xs: '6px', + sm: '8px', + md: '12px', + lg: '16px', + xl: '20px', + xxl: '24px', +} + +type IconMargins = keyof typeof iconTextMargins + +type Props = { + iconType: keyof IconType + iconSize: IconProps['size'] + iconColor?: keyof Theme['palette'] + margin?: IconMargins + color?: keyof Theme['palette'] + text: string + className?: string + iconSide?: 'left' | 'right' +} + +const LeftIconText = styled.div<{ margin: IconMargins }>` + display: flex; + align-items: center; + svg { + margin: 0 ${({ margin }) => iconTextMargins[margin]} 0 0; + } +` + +const RightIconText = styled.div<{ margin: IconMargins }>` + display: flex; + align-items: center; + svg { + margin: 0 0 0 ${({ margin }) => iconTextMargins[margin]}; + } +` + +/** + * The `IconText` renders an icon next to a text + */ +const IconText = ({ + iconSize, + margin = 'xs', + iconType, + iconColor, + text, + iconSide = 'left', + color, + className, +}: Props): React.ReactElement => { + return iconSide === 'right' ? ( + + {text} + + + ) : ( + + + {text} + + ) +} + +export default IconText diff --git a/apps/tx-builder/src/components/Link/index.tsx b/apps/tx-builder/src/components/Link/index.tsx new file mode 100644 index 000000000..444fb3df8 --- /dev/null +++ b/apps/tx-builder/src/components/Link/index.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import styled from 'styled-components' +import { type Theme } from '@material-ui/core/styles' + +export interface Props extends React.AnchorHTMLAttributes { + color?: keyof Theme['palette'] | 'white' +} + +const StyledLink = styled.a` + cursor: pointer; + color: ${({ theme, color = 'primary' }) => + color === 'white' ? theme.palette.common.white : theme.palette[color].dark}; + font-family: ${({ theme }) => theme.typography.fontFamily}; + text-decoration: underline; +` + +const Link: React.FC = ({ children, ...rest }): React.ReactElement => { + return {children} +} + +export default Link diff --git a/apps/tx-builder/src/components/Loader/index.tsx b/apps/tx-builder/src/components/Loader/index.tsx new file mode 100644 index 000000000..c0e6d75bf --- /dev/null +++ b/apps/tx-builder/src/components/Loader/index.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import styled from 'styled-components' +import CircularProgress from '@material-ui/core/CircularProgress' +import { type Theme } from '@material-ui/core/styles' + +const loaderSizes = { + xxs: '10px', + xs: '16px', + sm: '30px', + md: '50px', + lg: '70px', +} + +type Props = { + size: keyof typeof loaderSizes + color?: keyof Theme['palette'] + className?: string +} + +const StyledCircularProgress = styled( + ({ size, className }: Props): React.ReactElement => ( + + ), +)` + &.MuiCircularProgress-colorPrimary { + color: ${({ theme, color = 'primary' }) => theme.palette[color].main}; + } +` + +const Loader = ({ className, size, color }: Props): React.ReactElement => ( + +) + +export default Loader diff --git a/apps/tx-builder/src/components/QuickTip.tsx b/apps/tx-builder/src/components/QuickTip.tsx index ac1a46c48..b3382bc79 100644 --- a/apps/tx-builder/src/components/QuickTip.tsx +++ b/apps/tx-builder/src/components/QuickTip.tsx @@ -1,8 +1,7 @@ -import { Icon } from '@gnosis.pm/safe-react-components' import MuiAlert from '@material-ui/lab/Alert' import MuiAlertTitle from '@material-ui/lab/AlertTitle' -import React from 'react' import styled from 'styled-components' +import { Icon } from './Icon' type QuickTipProps = { onClose: () => void @@ -23,11 +22,10 @@ const QuickTip = ({ onClose }: QuickTipProps) => { const StyledAlert = styled(MuiAlert)` && { - font-family: 'Averta'; font-size: 14px; padding: 24px; - background: #eaf7f4; - color: #566976; + background: ${({ theme }) => theme.palette.secondary.background}; + color: ${({ theme }) => theme.palette.text.primary}; border-radius: 8px; .MuiAlert-action { diff --git a/apps/tx-builder/src/components/ShowMoreText.tsx b/apps/tx-builder/src/components/ShowMoreText.tsx index 83e150bbc..b920d99e3 100644 --- a/apps/tx-builder/src/components/ShowMoreText.tsx +++ b/apps/tx-builder/src/components/ShowMoreText.tsx @@ -1,5 +1,5 @@ import { useState, SyntheticEvent } from 'react' -import { Link } from '@gnosis.pm/safe-react-components' +import Link from './Link' type ShowMoreTextProps = { children: string diff --git a/apps/tx-builder/src/components/Switch.tsx b/apps/tx-builder/src/components/Switch.tsx new file mode 100644 index 000000000..dd07fc370 --- /dev/null +++ b/apps/tx-builder/src/components/Switch.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import SwitchMui from '@material-ui/core/Switch' +import styled from 'styled-components' +import { alpha } from '@material-ui/core/styles' + +const StyledSwitch = styled(({ ...rest }) => )` + && { + .MuiSwitch-thumb { + background: ${({ theme, checked }) => (checked ? '#12FF80' : theme.palette.common.white)}; + box-shadow: + 1px 1px 2px rgba(0, 0, 0, 0.2), + 0 0 1px rgba(0, 0, 0, 0.5); + } + + .MuiSwitch-track { + background: ${({ theme }) => theme.palette.common.black}; + } + + .MuiIconButton-label, + .MuiSwitch-colorSecondary.Mui-checked { + color: ${({ checked, theme }) => (checked ? theme.palette.secondary.dark : '#B2B5B2')}; + } + + .MuiSwitch-colorSecondary.Mui-checked:hover { + background-color: ${({ theme }) => alpha(theme.palette.secondary.dark, 0.08)}; + } + + .Mui-checked + .MuiSwitch-track { + background-color: ${({ theme }) => theme.palette.secondary.dark}; + } + } +` + +type Props = { + checked: boolean + onChange: (checked: boolean) => void +} + +const Switch = ({ checked, onChange }: Props): React.ReactElement => { + const onSwitchChange = (_event: any, checked: boolean) => onChange(checked) + + return +} + +export default Switch diff --git a/apps/tx-builder/src/components/Text.tsx b/apps/tx-builder/src/components/Text.tsx new file mode 100644 index 000000000..e3fa2062d --- /dev/null +++ b/apps/tx-builder/src/components/Text.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import Tooltip from '@material-ui/core/Tooltip' +import { withStyles, alpha } from '@material-ui/core/styles' +import { type Theme } from '@material-ui/core/styles' + +import { Typography, TypographyProps } from '@material-ui/core' +import styled from 'styled-components' + +type Props = { + children: React.ReactNode + tooltip?: string + color?: keyof Theme['palette'] | 'white' + className?: string + component?: 'span' | 'p' + strong?: boolean + center?: boolean +} + +const StyledTooltip = withStyles(theme => ({ + tooltip: { + backgroundColor: theme.palette.common.white, + color: theme.palette.text.primary, + boxShadow: `0px 0px 10px ${alpha('#28363D', 0.2)}`, + }, + arrow: { + color: theme.palette.common.white, + boxShadow: 'transparent', + }, +}))(Tooltip) + +const StyledTypography = styled(Typography)<{ $color?: keyof Theme['palette'] | 'white' } & Props>` + color: ${({ $color, theme }) => + $color + ? $color === 'white' + ? theme.palette.common.white + : theme.palette[$color].main + : theme.palette.text.primary}; + + ${({ center }) => center && 'text-align: center;'} + + ${({ strong }) => strong && `font-weight: bold;`} +` + +const Text = ({ + children, + component = 'p', + tooltip, + color, + ...rest +}: Props & Omit): React.ReactElement => { + const TextElement = ( + + {children} + + ) + + return tooltip === undefined ? ( + TextElement + ) : ( + + {TextElement} + + ) +} + +export default Text diff --git a/apps/tx-builder/src/components/Title.tsx b/apps/tx-builder/src/components/Title.tsx new file mode 100644 index 000000000..7600e6c20 --- /dev/null +++ b/apps/tx-builder/src/components/Title.tsx @@ -0,0 +1,72 @@ +import { BreakpointDefaults } from '@material-ui/core/styles/createBreakpoints' +import React from 'react' +import styled from 'styled-components' + +type Props = { + children: string | React.ReactNode + size: keyof BreakpointDefaults + withoutMargin?: boolean + strong?: boolean +} + +const StyledH1 = styled.h1<{ withoutMargin?: boolean; strong?: boolean }>` + font-family: ${({ theme }) => theme.legacy.fonts.fontFamily}; + font-size: ${({ theme }) => theme.legacy.title.size.xl.fontSize}; + line-height: ${({ theme }) => theme.legacy.title.size.xl.lineHeight}; + font-weight: ${({ strong }) => (strong ? 'bold' : 'normal')}; + margin: ${({ withoutMargin }) => (withoutMargin ? 0 : '30px')} 0; +` + +const StyledH2 = styled.h2<{ withoutMargin?: boolean; strong?: boolean }>` + font-family: ${({ theme }) => theme.legacy.fonts.fontFamily}; + font-size: ${({ theme }) => theme.legacy.title.size.lg.fontSize}; + line-height: ${({ theme }) => theme.legacy.title.size.lg.lineHeight}; + font-weight: ${({ strong }) => (strong ? 'bold' : 'normal')}; + margin: ${({ withoutMargin }) => (withoutMargin ? 0 : '28px')} 0; +` + +const StyledH3 = styled.h3<{ withoutMargin?: boolean; strong?: boolean }>` + font-family: ${({ theme }) => theme.legacy.fonts.fontFamily}; + font-size: ${({ theme }) => theme.legacy.title.size.md.fontSize}; + line-height: ${({ theme }) => theme.legacy.title.size.md.lineHeight}; + font-weight: ${({ strong }) => (strong ? 'bold' : 'normal')}; + margin: ${({ withoutMargin }) => (withoutMargin ? 0 : '26px')} 0; +` + +const StyledH4 = styled.h4<{ withoutMargin?: boolean; strong?: boolean }>` + font-family: ${({ theme }) => theme.legacy.fonts.fontFamily}; + font-size: ${({ theme }) => theme.legacy.title.size.sm.fontSize}; + line-height: ${({ theme }) => theme.legacy.title.size.sm.lineHeight}; + font-weight: ${({ strong }) => (strong ? 'bold' : 'normal')}; + margin: ${({ withoutMargin }) => (withoutMargin ? 0 : '22px')} 0; +` + +const StyledH5 = styled.h5<{ withoutMargin?: boolean; strong?: boolean }>` + font-family: ${({ theme }) => theme.legacy.fonts.fontFamily}; + font-size: ${({ theme }) => theme.legacy.title.size.xs.fontSize}; + line-height: ${({ theme }) => theme.legacy.title.size.xs.lineHeight}; + font-weight: ${({ strong }) => (strong ? 'bold' : 'normal')}; + margin: ${({ withoutMargin }) => (withoutMargin ? 0 : '18px')} 0; +` + +const Title = ({ children, size, ...rest }: Props) => { + switch (size) { + case 'xl': { + return {children} + } + case 'lg': { + return {children} + } + case 'md': { + return {children} + } + case 'sm': { + return {children} + } + case 'xs': { + return {children} + } + } +} + +export default Title diff --git a/apps/tx-builder/src/components/Tooltip.tsx b/apps/tx-builder/src/components/Tooltip.tsx new file mode 100644 index 000000000..5ab31a717 --- /dev/null +++ b/apps/tx-builder/src/components/Tooltip.tsx @@ -0,0 +1,108 @@ +import { ReactElement } from 'react' +import MUITooltip, { TooltipProps as TooltipPropsMui } from '@material-ui/core/Tooltip' +import { withStyles, alpha, type Theme } from '@material-ui/core/styles' +import { BreakpointDefaults } from '@material-ui/core/styles/createBreakpoints' +import { PaletteColor } from '@material-ui/core/styles/createPalette' + +type TooltipProps = { + size?: keyof BreakpointDefaults + backgroundColor?: keyof Theme['palette'] + textColor?: keyof Theme['palette'] + padding?: string + border?: string +} + +const getPaddingBySize = (size: keyof BreakpointDefaults): string => { + switch (size) { + case 'lg': + return '8px 16px' + default: + return '4px 8px' + } +} + +const getBorderBySize = (size: keyof BreakpointDefaults): string => { + switch (size) { + case 'lg': + return 'none' + default: + return `1px solid #B2B5B2` + } +} + +const getFontInfoBySize = ( + size: keyof BreakpointDefaults, +): { + fontSize: string + lineHeight: string +} => { + switch (size) { + case 'lg': + return { + fontSize: '14px', + lineHeight: '20px', + } + default: + return { + fontSize: '12px', + lineHeight: '16px', + } + } +} + +const customTooltip = ({ backgroundColor, textColor, size = 'md' }: TooltipProps) => + withStyles(theme => ({ + popper: { + zIndex: 2001, + }, + tooltip: { + backgroundColor: + backgroundColor && theme.palette[backgroundColor] + ? (theme.palette[backgroundColor] as PaletteColor).main + : theme.palette.primary.main, + boxShadow: `1px 2px 10px ${alpha('#28363D', 0.18)}`, + border: getBorderBySize(size), + color: textColor + ? (theme.palette[textColor] as PaletteColor).main + : theme.palette.background.default, + borderRadius: '4px', + fontFamily: theme.typography.fontFamily, + padding: getPaddingBySize(size), + fontSize: getFontInfoBySize(size).fontSize, + lineHeight: getFontInfoBySize(size).lineHeight, + }, + arrow: { + color: backgroundColor ? (theme.palette[backgroundColor] as PaletteColor).main : '#E8E7E6', + border: 'none', + + '&::before': { + boxShadow: `1px 2px 10px ${alpha('#28363D', 0.18)}`, + }, + }, + }))(MUITooltip) + +type Props = { + title: string + children: ReactElement +} & TooltipProps + +export const Tooltip = ({ + title, + backgroundColor, + textColor, + children, + size, + ...rest +}: Props & TooltipPropsMui): ReactElement => { + const StyledTooltip = customTooltip({ + backgroundColor, + textColor, + size, + }) + + return ( + + {children} + + ) +} diff --git a/apps/tx-builder/src/components/TransactionBatchListItem.tsx b/apps/tx-builder/src/components/TransactionBatchListItem.tsx index 53785d6ce..34f7dd808 100644 --- a/apps/tx-builder/src/components/TransactionBatchListItem.tsx +++ b/apps/tx-builder/src/components/TransactionBatchListItem.tsx @@ -1,13 +1,3 @@ -import { - Accordion, - AccordionSummary, - Dot, - EthHashInfo, - FixedIcon, - Icon, - Text, - Tooltip, -} from '@gnosis.pm/safe-react-components' import { AccordionDetails, IconButton } from '@material-ui/core' import { memo, useState } from 'react' import { DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd' @@ -16,6 +6,13 @@ import DragIndicatorIcon from '@material-ui/icons/DragIndicator' import { ProposedTransaction } from '../typings/models' import TransactionDetails from './TransactionDetails' import { getTransactionText } from '../utils' +import Text from './Text' +import { Accordion, AccordionSummary } from './Accordion' +import { Tooltip } from './Tooltip' +import EthHashInfo from './ETHHashInfo' +import { Icon } from './Icon' +import FixedIcon from './FixedIcon' +import Dot from './Dot' const UNKNOWN_POSITION_LABEL = '?' const minArrowSize = '12' @@ -86,8 +83,8 @@ const TransactionBatchListItem = memo( {/* Transacion Position */} - - {displayedTxPosition} + + {displayedTxPosition} {showArrowAdornment && } @@ -107,19 +104,13 @@ const TransactionBatchListItem = memo( > {/* Drag & Drop Indicator */} {reorderTransactions && ( - + )} {/* Destination Address label */} - {/* Transaction Description label */} - {transactionDescription} + {transactionDescription} {/* Transaction Actions */} {/* Edit transaction */} {replaceTransaction && ( - + + { event.stopPropagation() @@ -177,7 +162,6 @@ const TransactionBatchListItem = memo( placement="top" title="Expand transaction details" backgroundColor="primary" - textColor="white" arrow > - + )} @@ -240,7 +224,17 @@ const TransactionListItem = styled.li` margin-bottom: 8px; ` -// transaction postion dot styles +const StyledArrow = styled(FixedIcon)<{ isTxExpanded: boolean }>` + .icon-color { + fill: #b2b5b2; + } + ${({ isTxExpanded }) => + isTxExpanded && + ` + transform: rotateZ(180deg); + + `} +` const PositionWrapper = styled.div` display: flex; @@ -255,13 +249,13 @@ const PositionDot = styled(Dot).withConfig({ height: 24px; width: 24px; min-width: 24px; - background-color: ${({ isDragging }) => (isDragging ? '#92c9be' : ' #e2e3e3')}; + background-color: ${({ theme }) => theme.palette.border.light}; transition: background-color 0.5s linear; ` const ArrowAdornment = styled.div` position: relative; - border-left: 1px solid #e2e3e3; + border-left: 1px solid ${({ theme }) => theme.palette.border.light}; flex-grow: 1; margin-top: 8px; @@ -269,7 +263,7 @@ const ArrowAdornment = styled.div` content: ' '; display: inline-block; position: absolute; - border-left: 1px solid #e2e3e3; + border-left: 1px solid ${({ theme }) => theme.palette.border.light}; height: ${minArrowSize}px; bottom: -${minArrowSize}px; @@ -285,7 +279,7 @@ const ArrowAdornment = styled.div` border-width: 0 1px 1px 0; border-style: solid; - border-color: #e2e3e3; + border-color: ${({ theme }) => theme.palette.border.light}; padding: 3px; transform: rotate(45deg); @@ -301,27 +295,35 @@ const StyledAccordion = styled(Accordion).withConfig({ &.MuiAccordion-root { margin-bottom: 0; - border-color: ${({ isDragging, expanded }) => (isDragging || expanded ? '#92c9be' : '#e8e7e6')}; + border-width: 1px; + border-color: ${({ isDragging, expanded, theme }) => + isDragging || expanded ? theme.palette.secondary.light : theme.palette.background.paper}; transition: border-color 0.5s linear; + + &:hover { + border-color: ${({ theme }) => theme.palette.secondary.light}; + + .MuiAccordionSummary-root { + background-color: ${({ theme }) => theme.palette.secondary.background}; + } + } } .MuiAccordionSummary-root { height: 52px; padding: 0px 8px; - background-color: ${({ isDragging }) => (isDragging ? '#EFFAF8' : '#FFFFFF')}; - - &:hover { - background-color: #ffffff; - } + background-color: ${({ isDragging, theme }) => + isDragging ? theme.palette.secondary.background : theme.palette.background.paper}; .MuiIconButton-root { padding: 8px; } &.Mui-expanded { - background-color: #effaf8; - border-color: ${({ isDragging, expanded }) => - isDragging || expanded ? '#92c9be' : '#e8e7e6'}; + border-width: 1px; + background-color: ${({ theme }) => theme.palette.secondary.background}; + border-color: ${({ isDragging, expanded, theme }) => + isDragging || expanded ? theme.palette.secondary.light : '#e8e7e6'}; } } @@ -338,12 +340,15 @@ const TransactionActionButton = styled(IconButton)` ` const TransactionsDescription = styled(Text)` - flex-grow: 1; - padding-left: 24px; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + && { + flex-grow: 1; + padding-left: 24px; + font-size: 14px; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } ` const DragAndDropIndicatorIcon = styled(DragIndicatorIcon)` @@ -351,4 +356,10 @@ const DragAndDropIndicatorIcon = styled(DragIndicatorIcon)` margin-right: 4px; ` +const StyledEthHashInfo = styled(EthHashInfo)` + p { + font-size: 14px; + } +` + export default TransactionBatchListItem diff --git a/apps/tx-builder/src/components/TransactionDetails.tsx b/apps/tx-builder/src/components/TransactionDetails.tsx index 5aaeb1c67..46e3b209d 100644 --- a/apps/tx-builder/src/components/TransactionDetails.tsx +++ b/apps/tx-builder/src/components/TransactionDetails.tsx @@ -1,10 +1,13 @@ -import { ButtonLink, EthHashInfo, Text, Title } from '@gnosis.pm/safe-react-components' import React, { useEffect, useState } from 'react' import styled from 'styled-components' import useElementHeight from '../hooks/useElementHeight/useElementHeight' import { ProposedTransaction } from '../typings/models' import { weiToEther } from '../utils' +import EthHashInfo from './ETHHashInfo' +import Text from './Text' +import { Typography } from '@material-ui/core' +import ButtonLink from './buttons/ButtonLink' type TransactionDetailsProp = { transaction: ProposedTransaction @@ -29,13 +32,13 @@ const TransactionDetails = ({ transaction }: TransactionDetailsProp) => { return ( - + {isTokenTransferTx ? `Transfer ${weiToEther(value)} ${nativeCurrencySymbol} to:` : 'Interact with:'} - { {/* to address */} - - to (address) - - to (address) + { /> {/* value */} - - value: - + value: {`${weiToEther(value)} ${nativeCurrencySymbol}`} {/* data */} - - data: - + data: {data} {isContractInteractionTx && ( <> {/* method */} - - method: - - {contractMethod.name} + method: + {contractMethod.name} {/* method inputs */} {contractMethod.inputs.map(({ name, type }, index) => { @@ -84,7 +79,7 @@ const TransactionDetails = ({ transaction }: TransactionDetailsProp) => { return ( {/* input name */} - + {inputLabel} {/* input value */} @@ -115,11 +110,19 @@ const TxSummaryContainer = styled.div` margin-top: 16px; ` -const StyledTxTitle = styled(Title)` - font-size: 16px; - margin: 8px 0; - font-weight: bold; - line-height: initial; +const StyledTxTitle = styled(Typography)` + && { + font-size: 16px; + margin: 8px 0; + font-weight: bold; + line-height: initial; + } +` +const StyledText = styled(Text)` + && { + color: ${({ theme }) => theme.palette.text.secondary}; + font-weight: 400; + } ` const StyledMethodNameLabel = styled(Text)` @@ -152,7 +155,7 @@ const TxValueLabel = ({ children }: { children: React.ReactNode }) => { return (
{/* value */} - + {children} @@ -169,21 +172,28 @@ const TxValueLabel = ({ children }: { children: React.ReactNode }) => { const StyledTxValueLabel = styled(Text).withConfig({ shouldForwardProp: prop => !['showMore'].includes(prop) || !['showEllipsis'].includes(prop), })<{ showMore?: boolean; showEllipsis?: boolean }>` - max-height: ${({ showMore }) => (showMore ? '100%' : `${MAX_HEIGHT + 1}px`)}; - - line-break: anywhere; - overflow: hidden; - word-break: break-all; - text-overflow: ellipsis; - - ${({ showEllipsis, showMore }) => - !showMore && - showEllipsis && - `@supports (-webkit-line-clamp: 2) { + && { + max-height: ${({ showMore }) => (showMore ? '100%' : `${MAX_HEIGHT + 1}px`)}; + font-size: 14px; + line-break: anywhere; + overflow: hidden; + word-break: break-all; + text-overflow: ellipsis; + + ${({ showEllipsis, showMore }) => + !showMore && + showEllipsis && + `@supports (-webkit-line-clamp: 2) { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }`} + } +` +const StyledEthHashInfo = styled(EthHashInfo)` + p { + font-size: 14px; + } ` const StyledButtonLink = styled(ButtonLink)` diff --git a/apps/tx-builder/src/components/TransactionsBatchList.tsx b/apps/tx-builder/src/components/TransactionsBatchList.tsx index f2382b911..4b8075f17 100644 --- a/apps/tx-builder/src/components/TransactionsBatchList.tsx +++ b/apps/tx-builder/src/components/TransactionsBatchList.tsx @@ -1,5 +1,4 @@ import { isValidElement, useMemo, useState } from 'react' -import { Dot, Text, Title, Icon, Tooltip } from '@gnosis.pm/safe-react-components' import IconButton from '@material-ui/core/IconButton' import styled from 'styled-components' @@ -25,6 +24,11 @@ import Item from './TransactionBatchListItem' import VirtualizedList from './VirtualizedList' import { getTransactionText } from '../utils' import { EditableLabelProps } from './EditableLabel' +import Text from './Text' +import { Tooltip } from './Tooltip' +import { Icon } from './Icon' +import { Typography } from '@material-ui/core' +import Dot from './Dot' type TransactionsBatchListProps = { transactions: ProposedTransaction[] @@ -142,28 +146,16 @@ const TransactionsBatchList = ({ {showBatchHeader && ( {/* Transactions Batch Counter */} - - - {transactions.length} - + + {transactions.length} {/* Transactions Batch Title */} - {batchTitle && ( - - {batchTitle} - - )} + {batchTitle && {batchTitle}} {/* Transactions Batch Actions */} {saveBatch && ( - + )} {downloadBatch && ( - + downloadBatch(fileName, transactions)}> @@ -189,13 +175,7 @@ const TransactionsBatchList = ({ )} {removeAllTransactions && ( - + @@ -402,7 +382,6 @@ const TransactionsBatchWrapper = styled.section` // batch header styles const TransactionHeader = styled.header` - margin-top: 24px; display: flex; align-items: center; ` @@ -411,24 +390,29 @@ const TransactionCounterDot = styled(Dot)` height: 24px; width: 24px; min-width: 24px; - background-color: #566976; + + p { + color: ${({ theme }) => theme.palette.background.main}; + } ` -const TransactionsTitle = styled(Title)` - flex-grow: 1; - margin-left: 14px; - min-width: 0; +const TransactionsTitle = styled(Typography)` + && { + flex-grow: 1; + margin-left: 14px; + min-width: 0; - font-size: 16px; - line-height: normal; - display: flex; - align-items: center; + font-size: 16px; + line-height: normal; + display: flex; + align-items: center; + } ` const StyledHeaderIconButton = styled(IconButton)` &.MuiIconButton-root { border-radius: 4px; - background-color: white; + background-color: ${({ theme }) => theme.palette.code.main}; margin-left: 8px; } ` diff --git a/apps/tx-builder/src/components/Wrapper/index.tsx b/apps/tx-builder/src/components/Wrapper/index.tsx new file mode 100644 index 000000000..dc033d004 --- /dev/null +++ b/apps/tx-builder/src/components/Wrapper/index.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import styled from 'styled-components' + +function Wrapper({ children, centered }: { children: React.ReactNode; centered?: boolean }) { + return ( + +
{children}
+
+ ) +} + +const StyledWrapper = styled.main<{ centered?: boolean }>` + width: 100%; + min-height: 100%; + display: flex; + background: ${({ theme }) => theme.palette.background.main}; + color: ${({ theme }) => theme.palette.text.primary}; + + > section { + width: 100%; + padding: 120px 4rem 48px; + box-sizing: border-box; + margin: 0 auto; + max-width: ${({ centered }) => (centered ? '1000px' : '1500px')}; + } +` + +export default Wrapper diff --git a/apps/tx-builder/src/components/buttons/ButtonLink/index.tsx b/apps/tx-builder/src/components/buttons/ButtonLink/index.tsx new file mode 100644 index 000000000..001379080 --- /dev/null +++ b/apps/tx-builder/src/components/buttons/ButtonLink/index.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import styled from 'styled-components' +import { Icon, IconProps, IconType } from '../../Icon' +import Text from '../../Text' +import { TypographyProps } from '@material-ui/core' +import { type Theme } from '@material-ui/core/styles' + +export interface Props extends React.ComponentPropsWithoutRef<'button'> { + iconType?: keyof IconType + iconSize?: IconProps['size'] + textSize?: TypographyProps['variant'] + color: keyof Theme['palette'] + children?: React.ReactNode +} + +const StyledButtonLink = styled.button` + background: transparent; + border: none; + text-decoration: none; + cursor: pointer; + color: ${({ theme, color }) => theme.palette[color].main}; + font-family: ${({ theme }) => theme.typography.fontFamily}; + display: flex; + align-items: center; + + :focus { + outline: none; + } +` + +const StyledText = styled(Text)` + margin: 0 4px; +` + +const ButtonLink = ({ + iconType, + iconSize = 'md', + children, + textSize = 'body1', + ...rest +}: Props): React.ReactElement => { + return ( + + {iconType && } + + {children} + + + ) +} + +export default ButtonLink diff --git a/apps/tx-builder/src/components/buttons/CopyToClipboardBtn/copyTextToClipboard.ts b/apps/tx-builder/src/components/buttons/CopyToClipboardBtn/copyTextToClipboard.ts new file mode 100644 index 000000000..a5939018c --- /dev/null +++ b/apps/tx-builder/src/components/buttons/CopyToClipboardBtn/copyTextToClipboard.ts @@ -0,0 +1,24 @@ +const copyTextToClipboard = (text: string): void => { + const listener = (e: ClipboardEvent): void => { + e.preventDefault() + if (e.clipboardData) { + e.clipboardData.setData('text/plain', text) + } + } + + const range = document.createRange() + + const documentSelection = document.getSelection() + if (!documentSelection) { + return + } + + range.selectNodeContents(document.body) + documentSelection.addRange(range) + document.addEventListener('copy', listener) + document.execCommand('copy') + document.removeEventListener('copy', listener) + documentSelection.removeAllRanges() +} + +export default copyTextToClipboard diff --git a/apps/tx-builder/src/components/buttons/CopyToClipboardBtn/index.tsx b/apps/tx-builder/src/components/buttons/CopyToClipboardBtn/index.tsx new file mode 100644 index 000000000..9b0da7662 --- /dev/null +++ b/apps/tx-builder/src/components/buttons/CopyToClipboardBtn/index.tsx @@ -0,0 +1,78 @@ +import React, { useState } from 'react' +import styled from 'styled-components' + +import copyTextToClipboard from './copyTextToClipboard' +import { Icon } from '../../Icon' + +const StyledButton = styled.button` + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + border-radius: 50%; + transition: background-color 0.2s ease-in-out; + outline-color: transparent; + height: 24px; + width: 24px; + display: flex; + justify-content: center; + align-items: center; + :hover { + background-color: ${({ theme }) => theme.palette.divider}; + } +` + +type Props = { + textToCopy: string + className?: string + iconType?: Parameters[0]['type'] + tooltip?: string + tooltipAfterCopy?: string +} + +const CopyToClipboardBtn = ({ + className, + textToCopy, + iconType = 'copy', + tooltip = 'Copy to clipboard', +}: Props): React.ReactElement => { + const [clicked, setClicked] = useState(false) + + const copy = () => { + copyTextToClipboard(textToCopy) + setClicked(true) + } + + const onButtonClick = (event: React.MouseEvent): void => { + event.stopPropagation() + copy() + } + + const onKeyDown = (event: React.KeyboardEvent): void => { + // prevents event from bubbling when `Enter` is pressed + if (event.keyCode === 13) { + event.stopPropagation() + } + copy() + } + + const onButtonBlur = (): void => { + setTimeout((): void => setClicked(false), 300) + } + + return ( + + + + ) +} + +export default CopyToClipboardBtn diff --git a/apps/tx-builder/src/components/buttons/ExplorerButton/index.tsx b/apps/tx-builder/src/components/buttons/ExplorerButton/index.tsx new file mode 100644 index 000000000..f05c81833 --- /dev/null +++ b/apps/tx-builder/src/components/buttons/ExplorerButton/index.tsx @@ -0,0 +1,59 @@ +import React from 'react' +import styled from 'styled-components' +import { Icon } from '../../Icon' +import { ExplorerInfo } from '../../ETHHashInfo' + +const StyledLink = styled.a` + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + border-radius: 50%; + transition: background-color 0.2s ease-in-out; + outline-color: transparent; + height: 24px; + width: 24px; + display: flex; + justify-content: center; + align-items: center; + :hover { + background-color: #f0efee; + } +` + +type Props = { + className?: string + explorerUrl: ExplorerInfo +} + +const ExplorerButton = ({ className, explorerUrl }: Props): React.ReactElement => { + const { url, alt } = explorerUrl() + const onClick = (event: React.MouseEvent): void => { + event.stopPropagation() + } + + const onKeyDown = (event: React.KeyboardEvent): void => { + // prevents event from bubbling when `Enter` is pressed + if (event.keyCode === 13) { + event.stopPropagation() + } + } + + return ( + + + + ) +} + +export default ExplorerButton diff --git a/apps/tx-builder/src/components/buttons/Identicon/index.tsx b/apps/tx-builder/src/components/buttons/Identicon/index.tsx new file mode 100644 index 000000000..5ddaac4b2 --- /dev/null +++ b/apps/tx-builder/src/components/buttons/Identicon/index.tsx @@ -0,0 +1,32 @@ +import * as React from 'react' + +import makeBlockie from 'ethereum-blockies-base64' +import styled from 'styled-components' + +export const identiconSizes = { + xs: '10px', + sm: '16px', + md: '32px', + lg: '40px', + xl: '48px', + xxl: '60px', +} + +type Props = { + address: string + size: keyof typeof identiconSizes +} + +const StyledImg = styled.img<{ size: keyof typeof identiconSizes }>` + height: ${({ size }) => identiconSizes[size]}; + width: ${({ size }) => identiconSizes[size]}; + border-radius: 50%; +` + +const Identicon = ({ size = 'md', address, ...rest }: Props): React.ReactElement => { + const iconSrc = React.useMemo(() => makeBlockie(address), [address]) + + return +} + +export default Identicon diff --git a/apps/tx-builder/src/components/forms/AddNewTransactionForm.tsx b/apps/tx-builder/src/components/forms/AddNewTransactionForm.tsx index e5df78724..88fc08993 100644 --- a/apps/tx-builder/src/components/forms/AddNewTransactionForm.tsx +++ b/apps/tx-builder/src/components/forms/AddNewTransactionForm.tsx @@ -1,4 +1,3 @@ -import { Title, Button } from '@gnosis.pm/safe-react-components' import styled from 'styled-components' import { ContractInterface } from '../../typings/models' @@ -10,6 +9,9 @@ import SolidityForm, { parseFormToProposedTransaction, } from './SolidityForm' import { useTransactions, useNetwork } from '../../store' +import { Typography } from '@material-ui/core' +import Button from '../Button' +import FixedIcon from '../FixedIcon' type AddNewTransactionFormProps = { contract: ContractInterface | null @@ -43,7 +45,9 @@ const AddNewTransactionForm = ({ return ( <> - Transaction information + + Transaction information + {/* Add transaction btn */} - @@ -68,8 +73,19 @@ const AddNewTransactionForm = ({ export default AddNewTransactionForm +const StyledButtonLabel = styled.span` + margin-left: 8px; +` const ButtonContainer = styled.div` display: flex; justify-content: space-between; margin-top: 15px; + + .MuiButton-root { + padding-left: 10px; + } + + span { + display: flex; + } ` diff --git a/apps/tx-builder/src/components/forms/fields/AddressContractField.tsx b/apps/tx-builder/src/components/forms/fields/AddressContractField.tsx index 194c74be5..134bbf5ee 100644 --- a/apps/tx-builder/src/components/forms/fields/AddressContractField.tsx +++ b/apps/tx-builder/src/components/forms/fields/AddressContractField.tsx @@ -1,5 +1,5 @@ import { ReactElement } from 'react' -import { AddressInput } from '@gnosis.pm/safe-react-components' +import AddressInput from './AddressInput' const AddressContractField = ({ id, diff --git a/apps/tx-builder/src/components/forms/fields/AddressInput.tsx b/apps/tx-builder/src/components/forms/fields/AddressInput.tsx new file mode 100644 index 000000000..102c57016 --- /dev/null +++ b/apps/tx-builder/src/components/forms/fields/AddressInput.tsx @@ -0,0 +1,208 @@ +import React, { ReactElement, useState, ChangeEvent, useEffect, useCallback, useRef } from 'react' +import InputAdornment from '@material-ui/core/InputAdornment' +import CircularProgress from '@material-ui/core/CircularProgress' + +import { + addNetworkPrefix, + checksumAddress, + getAddressWithoutNetworkPrefix, + getNetworkPrefix, + isChecksumAddress, + isValidAddress, + isValidEnsName, +} from '../../../utils/address' +import TextFieldInput, { TextFieldInputProps } from './TextFieldInput' +import useThrottle from '../../../hooks/useThrottle' + +type AddressInputProps = { + name: string + address: string + networkPrefix?: string + showNetworkPrefix?: boolean + defaultValue?: string + disabled?: boolean + onChangeAddress: (address: string) => void + getAddressFromDomain?: (name: string) => Promise + customENSThrottleDelay?: number + showLoadingSpinner?: boolean +} & TextFieldInputProps + +function AddressInput({ + name, + address, + networkPrefix, + showNetworkPrefix = true, + disabled, + onChangeAddress, + getAddressFromDomain, + customENSThrottleDelay, + showLoadingSpinner, + InputProps, + inputProps, + hiddenLabel = false, + ...rest +}: AddressInputProps): ReactElement { + const [isLoadingENSResolution, setIsLoadingENSResolution] = useState(false) + const defaultInputValue = addPrefix(address, networkPrefix, showNetworkPrefix) + const inputRef = useRef({ value: defaultInputValue }) + const throttle = useThrottle() + + // we checksum & include the network prefix in the input if showNetworkPrefix is set to true + const updateInputValue = useCallback( + (value = '') => { + if (inputRef.current) { + const checksumAddress = checksumValidAddress(value) + inputRef.current.value = addPrefix(checksumAddress, networkPrefix, showNetworkPrefix) + } + }, + [networkPrefix, showNetworkPrefix], + ) + + const resolveDomainName = useCallback(async () => { + const isEnsName = isValidEnsName(address) + + if (isEnsName && getAddressFromDomain) { + try { + setIsLoadingENSResolution(true) + const resolvedAddress = await getAddressFromDomain(address) + onChangeAddress(checksumValidAddress(resolvedAddress)) + // we update the input value + updateInputValue(resolvedAddress) + } catch (e) { + onChangeAddress(address) + } finally { + setIsLoadingENSResolution(false) + } + } + }, [address, getAddressFromDomain, onChangeAddress, updateInputValue]) + + // ENS name resolution + useEffect(() => { + if (getAddressFromDomain) { + throttle(resolveDomainName, customENSThrottleDelay) + } + }, [getAddressFromDomain, resolveDomainName, customENSThrottleDelay, throttle]) + + // if address changes from outside (Like Loaded from a QR code) we update the input value + useEffect(() => { + const inputValue = inputRef.current?.value + const inputWithoutPrefix = getAddressWithoutNetworkPrefix(inputValue) + const addressWithoutPrefix = getAddressWithoutNetworkPrefix(address) + const inputPrefix = getNetworkPrefix(inputValue) + const addressPrefix = getNetworkPrefix(address) + + const isNewAddressLoaded = inputWithoutPrefix !== addressWithoutPrefix + const isNewPrefixLoaded = addressPrefix && inputPrefix !== addressPrefix + + // we check if we load a new address (both prefixed and unprefixed cases) + if (isNewAddressLoaded || isNewPrefixLoaded) { + // we update the input value + updateInputValue(address) + } + }, [address, updateInputValue]) + + // we trim, checksum & remove valid network prefix when a valid address is typed by the user + const updateAddressState = useCallback( + value => { + const inputValue = value.trim() + + const inputPrefix = getNetworkPrefix(inputValue) + const inputWithoutPrefix = getAddressWithoutNetworkPrefix(inputValue) + + // if the valid network prefix is present, we remove it from the address state + const isValidPrefix = networkPrefix === inputPrefix + const checksumAddress = checksumValidAddress(isValidPrefix ? inputWithoutPrefix : inputValue) + + onChangeAddress(checksumAddress) + }, + [networkPrefix, onChangeAddress], + ) + + // when user switch the network we update the address state + useEffect(() => { + // Because the `address` is going to change after we call `updateAddressState` + // To avoid calling `updateAddressState` twice, we check the value and the current address + const inputValue = inputRef.current?.value + if (inputValue !== address) { + updateAddressState(inputRef.current?.value) + } + }, [networkPrefix, address, updateAddressState]) + + // when user types we update the address state + function onChange(e: ChangeEvent) { + updateAddressState(e.target.value) + } + + const isLoading = isLoadingENSResolution || showLoadingSpinner + + const [shrink, setshrink] = useState(!!defaultInputValue) + + useEffect(() => { + setshrink(!!inputRef.current?.value) + }, [inputRef.current.value]) + + return ( + : InputProps?.endAdornment, + }} + inputProps={{ + ...inputProps, + ref: inputRef, + }} + InputLabelProps={{ + ...rest.InputLabelProps, + shrink: shrink || hiddenLabel || undefined, + }} + spellCheck={false} + {...rest} + /> + ) +} + +export default AddressInput + +function LoaderSpinnerAdornment() { + return ( + + + + ) +} + +// we only checksum valid addresses +function checksumValidAddress(address: string) { + if (isValidAddress(address) && !isChecksumAddress(address)) { + return checksumAddress(address) + } + + return address +} + +// we try to add the network prefix if its not present +function addPrefix( + address: string, + networkPrefix: string | undefined, + showNetworkPrefix = false, +): string { + if (!address) { + return '' + } + + if (showNetworkPrefix && networkPrefix) { + const hasPrefix = !!getNetworkPrefix(address) + + // if the address has not prefix we add it by default + if (!hasPrefix) { + return addNetworkPrefix(address, networkPrefix) + } + } + + return address +} diff --git a/apps/tx-builder/src/components/forms/fields/JsonField.tsx b/apps/tx-builder/src/components/forms/fields/JsonField.tsx index 8c4c86e77..736e66321 100644 --- a/apps/tx-builder/src/components/forms/fields/JsonField.tsx +++ b/apps/tx-builder/src/components/forms/fields/JsonField.tsx @@ -1,17 +1,12 @@ import { useState, useCallback, ClipboardEvent } from 'react' import styled from 'styled-components' -import { - Icon, - TextFieldInput, - Tooltip, - GenericModal, - Text, - Button, - IconTypes, -} from '@gnosis.pm/safe-react-components' import IconButton from '@material-ui/core/IconButton' -import { Box } from '@material-ui/core' +import { Box, Button, Tooltip } from '@material-ui/core' import useModal from '../../../hooks/useModal/useModal' +import { Icon, IconTypes } from '../../Icon' +import Text from '../../Text' +import GenericModal from '../../GenericModal' +import TextFieldInput from './TextFieldInput' const DEFAULT_ROWS = 4 @@ -113,17 +108,17 @@ const JsonField = ({ id, name, label, value, onChange }: Props) => { - Do you want to replace the current ABI? + Do you want to replace the current ABI? } onClose={toggleModal} title="Replace ABI" footer={ - - @@ -159,7 +154,7 @@ const IconContainerButton = ({ }) => ( - + ) @@ -173,9 +168,9 @@ const IconContainer = styled.div<{ error: boolean }>` top: -10px; right: 15px; border: 1px solid - ${({ theme, error }) => (error ? theme.colors.error : theme.colors.inputDefault)}; + ${({ theme, error }) => (error ? theme.palette.error.main : theme.palette.primary.main)}; border-radius: 50%; - background-color: #fff; + background-color: ${({ theme }) => theme.palette.code.main}; ` const StyledTextField = styled(TextFieldInput)` diff --git a/apps/tx-builder/src/components/forms/fields/SelectContractField.tsx b/apps/tx-builder/src/components/forms/fields/SelectContractField.tsx index c6016f586..45ddb69dd 100644 --- a/apps/tx-builder/src/components/forms/fields/SelectContractField.tsx +++ b/apps/tx-builder/src/components/forms/fields/SelectContractField.tsx @@ -1,7 +1,7 @@ import Autocomplete from '@mui/material/Autocomplete' -import { TextFieldInput } from '@gnosis.pm/safe-react-components' import { SelectItem } from '@gnosis.pm/safe-react-components/dist/inputs/Select' import { type SyntheticEvent, useCallback, useMemo } from 'react' +import TextFieldInput from './TextFieldInput' type SelectContractFieldTypes = { options: SelectItem[] diff --git a/apps/tx-builder/src/components/forms/fields/TextContractField.tsx b/apps/tx-builder/src/components/forms/fields/TextContractField.tsx index 544273d5d..693c50a72 100644 --- a/apps/tx-builder/src/components/forms/fields/TextContractField.tsx +++ b/apps/tx-builder/src/components/forms/fields/TextContractField.tsx @@ -1,6 +1,5 @@ -import { TextFieldInput } from '@gnosis.pm/safe-react-components' -import { TextFieldInputProps } from '@gnosis.pm/safe-react-components/dist/inputs/TextFieldInput' import styled from 'styled-components' +import TextFieldInput, { TextFieldInputProps } from './TextFieldInput' type TextContractFieldTypes = TextFieldInputProps & { networkPrefix?: undefined | string diff --git a/apps/tx-builder/src/components/forms/fields/TextFieldInput.tsx b/apps/tx-builder/src/components/forms/fields/TextFieldInput.tsx new file mode 100644 index 000000000..1f00e40fb --- /dev/null +++ b/apps/tx-builder/src/components/forms/fields/TextFieldInput.tsx @@ -0,0 +1,57 @@ +import React, { ReactElement } from 'react' +import TextFieldMui, { TextFieldProps } from '@material-ui/core/TextField' +import styled from 'styled-components' +import { errorStyles, inputLabelStyles, inputStyles } from './styles' + +export type TextFieldInputProps = { + id?: string + name: string + label: string + error?: string + helperText?: string | undefined + hiddenLabel?: boolean | undefined + showErrorsInTheLabel?: boolean | undefined +} & Omit + +function TextFieldInput({ + id, + name, + label, + error = '', + helperText, + value, + hiddenLabel, + showErrorsInTheLabel, + ...rest +}: TextFieldInputProps): ReactElement { + const hasError = !!error + + return ( + + ) +} + +const TextField = styled((props: TextFieldProps) => )` + && { + ${inputLabelStyles} + ${inputStyles} + ${errorStyles} + } +` + +export default TextFieldInput diff --git a/apps/tx-builder/src/components/forms/fields/TextareaContractField.tsx b/apps/tx-builder/src/components/forms/fields/TextareaContractField.tsx index f5be176ad..dce59f50d 100644 --- a/apps/tx-builder/src/components/forms/fields/TextareaContractField.tsx +++ b/apps/tx-builder/src/components/forms/fields/TextareaContractField.tsx @@ -1,5 +1,5 @@ -import { TextFieldInputProps } from '@gnosis.pm/safe-react-components/dist/inputs/TextFieldInput' import TextContractField from './TextContractField' +import { TextFieldInputProps } from './TextFieldInput' const DEFAULT_ROWS = 4 diff --git a/apps/tx-builder/src/components/forms/fields/styles.ts b/apps/tx-builder/src/components/forms/fields/styles.ts new file mode 100644 index 000000000..839bf797a --- /dev/null +++ b/apps/tx-builder/src/components/forms/fields/styles.ts @@ -0,0 +1,125 @@ +import { TextFieldProps } from '@material-ui/core' +import { css } from 'styled-components' + +export const inputLabelStyles = css` + &:hover { + .MuiInputLabel-root { + &.MuiInputLabel-shrink:not(.Mui-focused):not(.Mui-disabled) { + &.Mui-error { + color: ${({ theme }) => theme.palette.error.main}; + } + } + } + } + + .MuiInputLabel-root { + font-family: ${({ theme }) => theme.typography.fontFamily}; + color: ${({ theme }) => theme.palette.text.secondary}; + font-weight: 300; + font-size: 16px; + + &.MuiInputLabel-shrink { + color: ${({ theme }) => theme.palette.border.main}; + + &.Mui-error { + color: ${({ theme }) => theme.palette.error.main}; + } + } + &.Mui-disabled { + color: #dadada; + } + + /* Hide Label */ + ${({ hiddenLabel }) => + hiddenLabel + ? `border: 0; + border: 1px solid red; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px;` + : ''} + } +` + +export const inputStyles = css` + .MuiOutlinedInput-input:-webkit-autofill { + -webkit-text-fill-color: ${({ theme }) => theme.palette.text.primary}; + /* needs to use important because we have important styles being injected in this component */ + box-shadow: inset 0 0 0 100px ${({ theme }) => theme.palette.background.paper} !important; + } + + .MuiSvgIcon-root { + color: ${({ theme }) => theme.palette.text.primary}; + } + + .MuiOutlinedInput-root { + font-family: ${({ theme }) => theme.typography.fontFamily}; + color: ${({ theme }) => theme.palette.text.primary}; + /* Input */ + .MuiOutlinedInput-input { + &::placeholder, + &.Mui-disabled { + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'auto')}; + color: #b2bbc0; + } + } + + /* fieldset */ + .MuiOutlinedInput-notchedOutline { + ${({ hiddenLabel }) => (hiddenLabel ? 'top: 0' : '')}; + transition: border-color 0.2s ease-in-out; + border: 1px solid + ${({ theme, value }) => (value ? theme.palette.border.main : theme.palette.border.light)}; + border-radius: 6px; + legend { + display: ${({ hiddenLabel }) => (hiddenLabel ? 'none' : 'block')}; + } + } + + &:hover { + .MuiOutlinedInput-notchedOutline { + border-color: ${({ theme }) => theme.palette.border.light}; + } + } + + &.Mui-focused { + .MuiOutlinedInput-notchedOutline { + border-color: ${({ theme }) => theme.palette.border.light}; + } + &.Mui-error { + .MuiOutlinedInput-notchedOutline { + border-color: ${({ theme }) => theme.palette.error.main}; + } + } + } + &.Mui-disabled { + .MuiOutlinedInput-notchedOutline { + border-color: #dadada; + } + } + } + .MuiFormLabel-filled + + .MuiOutlinedInput-root:not(:hover):not(.Mui-disabled) + .MuiOutlinedInput-notchedOutline { + border-color: ${({ theme, error }) => + error ? theme.palette.error.main : theme.palette.border.light}; + } +` + +export const errorStyles = css` + .Mui-error { + &:hover, + .Mui-focused { + .MuiOutlinedInput-notchedOutline { + border-color: ${({ theme }) => theme.palette.error.main}; + } + } + .MuiOutlinedInput-notchedOutline { + border-color: ${({ theme }) => theme.palette.error.main}; + } + } +` diff --git a/apps/tx-builder/src/components/modals/DeleteBatchFromLibrary.tsx b/apps/tx-builder/src/components/modals/DeleteBatchFromLibrary.tsx index 3409f8a14..feed76d69 100644 --- a/apps/tx-builder/src/components/modals/DeleteBatchFromLibrary.tsx +++ b/apps/tx-builder/src/components/modals/DeleteBatchFromLibrary.tsx @@ -1,8 +1,11 @@ -import { Dot, Text, Button, GenericModal } from '@gnosis.pm/safe-react-components' import Box from '@material-ui/core/Box' import styled from 'styled-components' import { Batch } from '../../typings/models' +import GenericModal from '../GenericModal' +import Text from '../Text' +import Button from '../Button' +import Dot from '../Dot' type DeleteBatchFromLibraryProps = { batch: Batch @@ -17,28 +20,21 @@ const DeleteBatchFromLibrary = ({ batch, onClick, onClose }: DeleteBatchFromLibr withoutBodyPadding body={ - - - {batch.transactions.length} - + + {batch.transactions.length} - {`${batch.name} batch will be permanently deleted`} + {`${batch.name} batch will be permanently deleted`} - - @@ -58,13 +54,14 @@ const StyledModalBodyWrapper = styled.div` ` const StyledModalDot = styled(Dot)` - height: 24px; - width: 24px; - min-width: 24px; - background-color: #566976; + && { + height: 24px; + width: 24px; + min-width: 24px; - position: absolute; - top: 22px; + position: absolute; + top: 22px; + } ` const StyledModalText = styled(Text)` diff --git a/apps/tx-builder/src/components/modals/DeleteBatchModal.tsx b/apps/tx-builder/src/components/modals/DeleteBatchModal.tsx index 00a61cdeb..78e5bc7c6 100644 --- a/apps/tx-builder/src/components/modals/DeleteBatchModal.tsx +++ b/apps/tx-builder/src/components/modals/DeleteBatchModal.tsx @@ -1,6 +1,9 @@ -import { Dot, Text, Button, GenericModal } from '@gnosis.pm/safe-react-components' import Box from '@material-ui/core/Box' import styled from 'styled-components' +import GenericModal from '../GenericModal' +import Text from '../Text' +import Button from '../Button' +import Dot from '../Dot' type DeleteBatchModalProps = { count: number @@ -15,25 +18,21 @@ const DeleteBatchModal = ({ count, onClick, onClose }: DeleteBatchModalProps) => withoutBodyPadding body={ - - - {count} - + + {count} - - {`transaction${count > 1 ? 's' : ''}`} will be cleared - + {`transaction${count > 1 ? 's' : ''}`} will be cleared - - @@ -56,7 +55,6 @@ const StyledModalDot = styled(Dot)` height: 24px; width: 24px; min-width: 24px; - background-color: #566976; position: absolute; top: 22px; diff --git a/apps/tx-builder/src/components/modals/DeleteTransactionModal.tsx b/apps/tx-builder/src/components/modals/DeleteTransactionModal.tsx index fd0bc20bc..b610ea845 100644 --- a/apps/tx-builder/src/components/modals/DeleteTransactionModal.tsx +++ b/apps/tx-builder/src/components/modals/DeleteTransactionModal.tsx @@ -1,6 +1,9 @@ -import { Dot, Text, Button, GenericModal } from '@gnosis.pm/safe-react-components' import Box from '@material-ui/core/Box' import styled from 'styled-components' +import GenericModal from '../GenericModal' +import Text from '../Text' +import Button from '../Button' +import Dot from '../Dot' type DeleteTransactionModalProps = { txIndex: number @@ -22,23 +25,21 @@ const DeleteTransactionModal = ({ withoutBodyPadding body={ - - - {positionLabel} - + + {positionLabel} - {`${txDescription} will be permanently deleted from the batch`} + {`${txDescription} will be permanently deleted from the batch`} - - @@ -61,7 +62,6 @@ const StyledModalDot = styled(Dot)` height: 24px; width: 24px; min-width: 24px; - background-color: #566976; position: absolute; top: 22px; diff --git a/apps/tx-builder/src/components/modals/EditTransactionModal.tsx b/apps/tx-builder/src/components/modals/EditTransactionModal.tsx index 203e5b0b9..94663bf83 100644 --- a/apps/tx-builder/src/components/modals/EditTransactionModal.tsx +++ b/apps/tx-builder/src/components/modals/EditTransactionModal.tsx @@ -1,4 +1,3 @@ -import { GenericModal, Button } from '@gnosis.pm/safe-react-components' import styled from 'styled-components' import { ProposedTransaction } from '../../typings/models' import SolidityForm, { @@ -11,6 +10,8 @@ import SolidityForm, { TO_ADDRESS_FIELD_NAME, } from '../forms/SolidityForm' import { weiToEther } from '../../utils' +import GenericModal from '../GenericModal' +import Button from '../Button' type EditTransactionModalProps = { txIndex: number @@ -78,12 +79,12 @@ const EditTransactionModal = ({ > {/* Remove transaction btn */} - {/* Add transaction btn */} - @@ -106,7 +107,7 @@ const FormContainer = styled.div` padding: 24px; border-radius: 8px; - background-color: white; + background-color: ${({ theme }) => theme.palette.background.paper}; ` export default EditTransactionModal diff --git a/apps/tx-builder/src/components/modals/ImplementationABIDialog.tsx b/apps/tx-builder/src/components/modals/ImplementationABIDialog.tsx index 7cd0c2f6a..1f7ef59e2 100644 --- a/apps/tx-builder/src/components/modals/ImplementationABIDialog.tsx +++ b/apps/tx-builder/src/components/modals/ImplementationABIDialog.tsx @@ -1,7 +1,10 @@ import React from 'react' -import { Text, Button, GenericModal, EthHashInfo } from '@gnosis.pm/safe-react-components' import Box from '@material-ui/core/Box' import styled from 'styled-components' +import Text from '../Text' +import Button from '../Button' +import EthHashInfo from '../ETHHashInfo' +import GenericModal from '../GenericModal' type Props = { networkPrefix: string @@ -24,9 +27,7 @@ const ImplementationABIDialog: React.FC = ({ withoutBodyPadding body={ - - The contract looks like a proxy. Do you want to use the Implementation ABI? - + The contract looks like a proxy. Do you want to use the Implementation ABI? = ({ justifyContent="center" maxWidth="470px" > - - diff --git a/apps/tx-builder/src/components/modals/SaveBatchModal.tsx b/apps/tx-builder/src/components/modals/SaveBatchModal.tsx index 88c1f6f89..6fdf0fa64 100644 --- a/apps/tx-builder/src/components/modals/SaveBatchModal.tsx +++ b/apps/tx-builder/src/components/modals/SaveBatchModal.tsx @@ -1,4 +1,3 @@ -import { Button, GenericModal } from '@gnosis.pm/safe-react-components' import Box from '@material-ui/core/Box' import { useForm, ValidateResult } from 'react-hook-form' import { useNavigate } from 'react-router-dom' @@ -9,6 +8,8 @@ import { useTransactionLibrary } from '../../store' import { Batch } from '../../typings/models' import Field from '../forms/fields/Field' import { TEXT_FIELD_TYPE } from '../forms/fields/fields' +import GenericModal from '../GenericModal' +import Button from '../Button' type SaveBatchModalProps = { onClick: (name: string) => void @@ -53,8 +54,8 @@ const SaveBatchModal = ({ onClick, onClose }: SaveBatchModalProps) => { control={control} showErrorsInTheLabel={false} /> - - diff --git a/apps/tx-builder/src/components/modals/SuccessBatchCreationModal.tsx b/apps/tx-builder/src/components/modals/SuccessBatchCreationModal.tsx index 22c23adb2..9980ceaf0 100644 --- a/apps/tx-builder/src/components/modals/SuccessBatchCreationModal.tsx +++ b/apps/tx-builder/src/components/modals/SuccessBatchCreationModal.tsx @@ -1,8 +1,12 @@ -import { Dot, Text, Button, GenericModal, Title } from '@gnosis.pm/safe-react-components' import Box from '@material-ui/core/Box' import styled from 'styled-components' import { ReactComponent as SuccessBatchSVG } from '../../assets/success-batch.svg' +import GenericModal from '../GenericModal' +import Text from '../Text' +import Button from '../Button' +import { Typography } from '@material-ui/core' +import Dot from '../Dot' type SuccessBatchCreationModalProps = { count: number @@ -26,25 +30,21 @@ const SuccessBatchCreationModal = ({ count, onClick, onClose }: SuccessBatchCrea {/* Title */} - Success! + Success! {/* Text */} - - - {count} - + + {count} - Transaction Batch in the queue. + Transaction Batch in the queue. - You can now sign and execute it. + You can now sign and execute it. {/* Button */} - + } onClose={onClose} @@ -58,9 +58,11 @@ const StyledBodyWrapper = styled(Box)` padding: 50px; ` -const StyledBodyTitle = styled(Title)` - font-size: 32px; - margin: 16px 0; +const StyledBodyTitle = styled(Typography)` + && { + font-size: 32px; + margin: 16px 0; + } ` const StyledTextWrapper = styled.div` @@ -74,8 +76,6 @@ const StyledModalDot = styled(Dot)` width: 24px; min-width: 24px; top: -1px; - - background-color: #566976; ` const StyledModalText = styled(Text)` diff --git a/apps/tx-builder/src/components/modals/WrongChainBatchModal.tsx b/apps/tx-builder/src/components/modals/WrongChainBatchModal.tsx index c6f79b22f..fe40f3f1c 100644 --- a/apps/tx-builder/src/components/modals/WrongChainBatchModal.tsx +++ b/apps/tx-builder/src/components/modals/WrongChainBatchModal.tsx @@ -1,6 +1,9 @@ -import { Button, GenericModal, Icon, Text } from '@gnosis.pm/safe-react-components' import Box from '@material-ui/core/Box' import styled from 'styled-components' +import GenericModal from '../GenericModal' +import { Icon } from '../Icon' +import Text from '../Text' +import Button from '../Button' type WrongChainBatchModalProps = { onClick: () => void @@ -25,7 +28,7 @@ const WrongChainBatchModal = ({ onClick, onClose, fileChainId }: WrongChainBatch withoutBodyPadding body={ - + This batch is from another Chain {fileChainId ? ` (${fileChainId})` : ''}! @@ -35,7 +38,7 @@ const WrongChainBatchModal = ({ onClick, onClose, fileChainId }: WrongChainBatch justifyContent="center" maxWidth={'450px'} > - diff --git a/apps/tx-builder/src/global.ts b/apps/tx-builder/src/global.ts index db2e02198..31d967e3b 100644 --- a/apps/tx-builder/src/global.ts +++ b/apps/tx-builder/src/global.ts @@ -1,6 +1,6 @@ import { createGlobalStyle } from 'styled-components' -import avertaFont from '@gnosis.pm/safe-react-components/dist/fonts/averta-normal.woff2' -import avertaBoldFont from '@gnosis.pm/safe-react-components/dist/fonts/averta-bold.woff2' +import DMSansFont from './assets/fonts/DMSansRegular.woff2' +import DMSansBoldFont from './assets/fonts/DMSans700.woff2' const GlobalStyle = createGlobalStyle` html { @@ -17,12 +17,19 @@ const GlobalStyle = createGlobalStyle` #root { height: 100%; } + + @font-face { + font-family: 'DM Sans'; + font-display: swap; + font-weight: 400; + src: url(${DMSansFont}) format('woff2'); + } @font-face { - font-family: 'Averta'; - src: local('Averta'), local('Averta Bold'), - url(${avertaFont}) format('woff2'), - url(${avertaBoldFont}) format('woff'); + font-family: 'DM Sans'; + font-display: swap; + font-weight: bold; + src: url(${DMSansBoldFont}) format('woff2'); } input:-webkit-autofill, diff --git a/apps/tx-builder/src/hooks/useDebounce.ts b/apps/tx-builder/src/hooks/useDebounce.ts new file mode 100644 index 000000000..18d25366e --- /dev/null +++ b/apps/tx-builder/src/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +import { useEffect, useState } from 'react' + +const useDebounce = (value: T, delay: number): T => { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const timer = setTimeout(() => setDebouncedValue(value), delay) + return () => clearTimeout(timer) + }, [value, delay]) + + return debouncedValue +} + +export default useDebounce diff --git a/apps/tx-builder/src/hooks/useThrottle.ts b/apps/tx-builder/src/hooks/useThrottle.ts new file mode 100644 index 000000000..b5c91f7d8 --- /dev/null +++ b/apps/tx-builder/src/hooks/useThrottle.ts @@ -0,0 +1,26 @@ +import { useRef, useCallback } from 'react' + +const DEFAULT_DELAY = 650 + +type ThrottleType = (callback: Function, delay?: number) => void + +const useThrottle: () => ThrottleType = () => { + const timerRefId = useRef | undefined>() + + const throttle = useCallback((callback, delay = DEFAULT_DELAY) => { + // If setTimeout is already scheduled, clearTimeout + if (timerRefId.current) { + clearTimeout(timerRefId.current) + } + + // Schedule the exec after a delay + timerRefId.current = setTimeout(function () { + timerRefId.current = undefined + return callback() + }, delay) + }, []) + + return throttle +} + +export default useThrottle diff --git a/apps/tx-builder/src/index.tsx b/apps/tx-builder/src/index.tsx index 13b2b2994..87c6ebae7 100644 --- a/apps/tx-builder/src/index.tsx +++ b/apps/tx-builder/src/index.tsx @@ -1,6 +1,4 @@ import ReactDOM from 'react-dom' -import { ThemeProvider } from 'styled-components' -import { theme } from '@gnosis.pm/safe-react-components' import { SafeProvider } from '@safe-global/safe-apps-react-sdk' import { BrowserRouter } from 'react-router-dom' @@ -9,19 +7,25 @@ import * as serviceWorker from './serviceWorker' import GlobalStyles from './global' import App from './App' import StoreProvider from './store' +import SafeThemeProvider from './theme/SafeThemeProvider' +import { ThemeProvider } from 'styled-components' ReactDOM.render( <> - - - - - - - - - + + {theme => ( + + + + + + + + + + )} + , document.getElementById('root'), ) diff --git a/apps/tx-builder/src/pages/CreateTransactions.tsx b/apps/tx-builder/src/pages/CreateTransactions.tsx index 6cfd73535..db164ceba 100644 --- a/apps/tx-builder/src/pages/CreateTransactions.tsx +++ b/apps/tx-builder/src/pages/CreateTransactions.tsx @@ -1,15 +1,16 @@ import { useState } from 'react' import { useNavigate } from 'react-router-dom' import styled from 'styled-components' -import { Button, Tooltip } from '@gnosis.pm/safe-react-components' import Grid from '@material-ui/core/Grid' -import TransactionsBatchList from '../components/TransactionsBatchList' -import CreateNewBatchCard from '../components/CreateNewBatchCard' import { CREATE_BATCH_PATH, REVIEW_AND_CONFIRM_PATH } from '../routes/routes' import QuickTip from '../components/QuickTip' import { useNetwork, useTransactionLibrary, useTransactions } from '../store' import useModal from '../hooks/useModal/useModal' +import Button from '../components/Button' +import { Tooltip } from '../components/Tooltip' +import CreateNewBatchCard from '../components/CreateNewBatchCard' +import TransactionsBatchList from '../components/TransactionsBatchList' import WrongChainBatchModal from '../components/modals/WrongChainBatchModal' const CreateTransactions = () => { @@ -56,7 +57,6 @@ const CreateTransactions = () => { /> {/* Go to Review Screen button */} )} @@ -139,21 +129,17 @@ const ReviewAndConfirm = () => { {simulationRequestStatus === FETCH_STATUS.ERROR && ( - - An unexpected error occurred during simulation. - + An unexpected error occurred during simulation. )} {simulationRequestStatus === FETCH_STATUS.LOADING && ( <> - - Running simulation... - + Running simulation... )} @@ -166,15 +152,14 @@ const ReviewAndConfirm = () => { iconType="alert" iconColor="error" text="Failed" - textSize="lg" color="error" /> - + The batch failed during the simulation throwing error{' '} {simulation.transaction.error_message} in the contract at{' '} {simulation.transaction.error_info?.address}. Full simulation report is available{' '} - + on Tenderly . @@ -188,12 +173,11 @@ const ReviewAndConfirm = () => { iconType="check" iconColor="primary" text="Success" - textSize="lg" color="primary" /> - + The batch was successfully simulated. Full simulation report is available{' '} - + on Tenderly . @@ -236,21 +220,24 @@ const ReviewAndConfirm = () => { export default ReviewAndConfirm const StyledButton = styled(ButtonLink)` - position: absolute; - right: 26px; - padding: 5px; - width: 26px; - height: 26px; + && { + position: absolute; + right: 26px; + padding: 5px; + width: 26px; + height: 26px; - :hover { - background: ${({ theme }) => theme.colors.separator}; - border-radius: 16px; + :hover { + background: ${({ theme }) => theme.palette.divider}; + border-radius: 16px; + } } ` const SimulationContainer = styled(Card)` box-shadow: none; margin: 24px 0 0 34px; + background: ${({ theme }) => theme.palette.background.paper}; // last child is the status result & > :last-child { @@ -258,21 +245,16 @@ const SimulationContainer = styled(Card)` } ` -const Wrapper = styled.main` +const StyledTitle = styled(Typography)` && { - padding: 120px 48px 48px; - max-width: 650px; - margin: 0 auto; + margin-top: 0px; + margin-bottom: 1rem; + font-size: 20px; + font-weight: 700; + line-height: normal; } ` -const StyledTitle = styled(Title)` - margin-top: 0px; - margin-bottom: 5px; - font-size: 20px; - line-height: normal; -` - const ButtonsWrapper = styled.div` display: flex; margin-top: 24px; diff --git a/apps/tx-builder/src/pages/SaveTransactionLibrary.tsx b/apps/tx-builder/src/pages/SaveTransactionLibrary.tsx index 5808ed672..10ea2a26d 100644 --- a/apps/tx-builder/src/pages/SaveTransactionLibrary.tsx +++ b/apps/tx-builder/src/pages/SaveTransactionLibrary.tsx @@ -2,12 +2,12 @@ import { useEffect } from 'react' import { useNavigate } from 'react-router-dom' import Grid from '@material-ui/core/Grid' import styled from 'styled-components' -import { Button } from '@gnosis.pm/safe-react-components' import TransactionsBatchList from '../components/TransactionsBatchList' import { useTransactionLibrary, useTransactions } from '../store' import { CREATE_BATCH_PATH, REVIEW_AND_CONFIRM_PATH, SAVE_BATCH_PATH } from '../routes/routes' import EditableLabel from '../components/EditableLabel' +import Button from '../components/Button' const SaveTransactionLibrary = () => { const { @@ -52,7 +52,6 @@ const SaveTransactionLibrary = () => { /> {/* Go to Review Screen button */}