diff --git a/packages/extension-ui/src/Popup/Accounts/Account.test.tsx b/packages/extension-ui/src/Popup/Accounts/Account.test.tsx index 020998370d9..1a3d06e7e4f 100644 --- a/packages/extension-ui/src/Popup/Accounts/Account.test.tsx +++ b/packages/extension-ui/src/Popup/Accounts/Account.test.tsx @@ -4,7 +4,7 @@ import Adapter from 'enzyme-adapter-react-16'; import { configure, mount, ReactWrapper } from 'enzyme'; -import { Link, defaultTheme, Theme } from '@polkadot/extension-ui/components'; +import { defaultTheme, Theme } from '@polkadot/extension-ui/components'; import { MemoryRouter } from 'react-router'; import React from 'react'; @@ -28,18 +28,20 @@ describe('Account component', () => { it('shows Export option if account is not external', () => { wrapper = mountAccountComponent({ isExternal: false }); + wrapper.find('Details').simulate('click'); - expect(wrapper.find(Link).length).toBe(3); - expect(wrapper.find(Link).at(0).text()).toContain('Forget'); - expect(wrapper.find(Link).at(1).text()).toContain('Export'); - expect(wrapper.find(Link).at(2).text()).toContain('Edit'); + expect(wrapper.find('MenuItem').length).toBe(3); + expect(wrapper.find('MenuItem').at(0).text()).toBe('Rename'); + expect(wrapper.find('MenuItem').at(1).text()).toBe('Export Account'); + expect(wrapper.find('MenuItem').at(2).text()).toBe('Forget Account'); }); it('does not show Export option if account is external', () => { wrapper = mountAccountComponent({ isExternal: true }); + wrapper.find('Details').simulate('click'); - expect(wrapper.find(Link).length).toBe(2); - expect(wrapper.find(Link).at(0).text()).toContain('Forget'); - expect(wrapper.find(Link).at(1).text()).toContain('Edit'); + expect(wrapper.find('MenuItem').length).toBe(2); + expect(wrapper.find('MenuItem').at(0).text()).toBe('Rename'); + expect(wrapper.find('MenuItem').at(1).text()).toBe('Forget Account'); }); }); diff --git a/packages/extension-ui/src/Popup/Accounts/Account.tsx b/packages/extension-ui/src/Popup/Accounts/Account.tsx index 7cff269aed1..c0753e6891e 100644 --- a/packages/extension-ui/src/Popup/Accounts/Account.tsx +++ b/packages/extension-ui/src/Popup/Accounts/Account.tsx @@ -7,7 +7,7 @@ import { AccountJson } from '@polkadot/extension/background/types'; import React, { useContext, useState } from 'react'; import styled from 'styled-components'; -import { ActionBar, ActionContext, Address, Link } from '../../components'; +import { ActionContext, Address, Link } from '../../components'; import { editAccount } from '../../messaging'; import { Name } from '../../partials'; @@ -32,11 +32,19 @@ function Account ({ address, className, isExternal }: Props): React.ReactElement _toggleEdit(); }; - return ( + return
+ + Rename + + {!isExternal && Export Account} + Forget Account + + } > {isEditing && ( )} - -
- Forget - {!isExternal && Export} -
- Edit -
- ); +
; } +const MenuGroup = styled.div` + padding-bottom: 16px; + margin-bottom: 16px; + border-bottom: 1px solid #222222; +`; + +const MenuItem = styled(Link)` + padding: 4px 16px; + display: block; + border-radius: 8px; + font-weight: 600; + font-size: 15px; + line-height: 20px; +`; + +MenuItem.displayName = 'MenuItem'; + export default styled(Account)` .edit-name { - left: 3rem; position: absolute; - right: 0.75rem; - top: -0.5rem; + left: 73px; + top: 8px; } `; diff --git a/packages/extension-ui/src/Popup/Accounts/index.tsx b/packages/extension-ui/src/Popup/Accounts/index.tsx index ee06426e720..e21b2b5d057 100644 --- a/packages/extension-ui/src/Popup/Accounts/index.tsx +++ b/packages/extension-ui/src/Popup/Accounts/index.tsx @@ -11,7 +11,8 @@ import { Header, MediaContext, AddAccount, - ButtonArea + ButtonArea, + Svg } from '../../components'; import Account from './Account'; import styled from 'styled-components'; @@ -39,13 +40,9 @@ const ButtonWithSubtitle = styled(Button)` const QrButton = styled(Button)` width: 60px; - span { + ${Svg} { width: 20px; height: 20px; - display: block; - mask: url(${QrImage}); - mask-size: cover; - background: ${({ theme }): string => theme.color}; } `; @@ -94,7 +91,7 @@ export default function Accounts (): React.ReactElement { {mediaAllowed && ( - + )} diff --git a/packages/extension-ui/src/assets/copy.svg b/packages/extension-ui/src/assets/copy.svg index 464cd6b6193..55640424c63 100644 --- a/packages/extension-ui/src/assets/copy.svg +++ b/packages/extension-ui/src/assets/copy.svg @@ -1,3 +1,3 @@ - + diff --git a/packages/extension-ui/src/assets/details.svg b/packages/extension-ui/src/assets/details.svg new file mode 100644 index 00000000000..b9f26c99b7d --- /dev/null +++ b/packages/extension-ui/src/assets/details.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/extension-ui/src/assets/print.svg b/packages/extension-ui/src/assets/print.svg index e0ce61492e5..a666fcef18a 100644 --- a/packages/extension-ui/src/assets/print.svg +++ b/packages/extension-ui/src/assets/print.svg @@ -1,3 +1,3 @@ - + diff --git a/packages/extension-ui/src/components/ActionText.tsx b/packages/extension-ui/src/components/ActionText.tsx index 0085b68db2a..e60ce0b7087 100644 --- a/packages/extension-ui/src/components/ActionText.tsx +++ b/packages/extension-ui/src/components/ActionText.tsx @@ -3,6 +3,7 @@ // of the Apache-2.0 license. See the LICENSE file for details. import React, { MouseEventHandler } from 'react'; +import Svg from '@polkadot/extension-ui/components/Svg'; import styled from 'styled-components'; interface Props { @@ -15,7 +16,7 @@ interface Props { function ActionText ({ icon, className, text, onClick }: Props): React.ReactElement { return (
- {icon && } + {icon && } {text}
); @@ -23,19 +24,21 @@ function ActionText ({ icon, className, text, onClick }: Props): React.ReactElem export default styled(ActionText)` cursor: pointer; + + span { + font-size: ${({ theme }): string => theme.labelFontSize}; + line-height: ${({ theme }): string => theme.labelLineHeight}; + text-decoration-line: underline; + color: ${({ theme }): string => theme.labelColor} + } - img { + ${Svg} { + background: ${({ theme }): string => theme.iconLabelColor}; + display: inline-block; position: relative; top: 2px; width: 14px; height: 14px; margin-right: 6px; } - - span { - font-size: ${({ theme }): string => theme.labelFontSize}; - line-height: ${({ theme }): string => theme.labelLineHeight}; - text-decoration-line: underline; - color: ${({ theme }): string => theme.labelColor} - } `; diff --git a/packages/extension-ui/src/components/Address.tsx b/packages/extension-ui/src/components/Address.tsx index aed78083088..9fc4e0116d8 100644 --- a/packages/extension-ui/src/components/Address.tsx +++ b/packages/extension-ui/src/components/Address.tsx @@ -5,22 +5,26 @@ import { AccountJson } from '@polkadot/extension/background/types'; import { Chain } from '@polkadot/extension-chains/types'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import findChain from '@polkadot/extension-chains'; import settings from '@polkadot/ui-settings'; import { decodeAddress, encodeAddress } from '@polkadot/util-crypto'; -import IconBox from './IconBox'; import { AccountContext } from './contexts'; -import { Identicon } from '@polkadot/extension-ui/components'; +import Identicon from '@polkadot/extension-ui/components/Identicon'; +import Svg from '@polkadot/extension-ui/components/Svg'; +import Menu from '@polkadot/extension-ui/components/Menu'; +import DetailsImg from '../assets/details.svg'; +import { useOutsideClick } from '@polkadot/extension-ui/hooks'; interface Props { address?: string | null; - children?: React.ReactNode; className?: string; name?: React.ReactNode | null; + children?: React.ReactNode; genesisHash?: string | null; + actions?: React.ReactNode; } // find an account in our list @@ -49,17 +53,19 @@ function recodeAddress (address: string, accounts: AccountJson[], genesisHash?: ]; } -function Address ({ address, children, className, genesisHash, name }: Props): React.ReactElement { +function Address ({ address, className, children, genesisHash, name, actions }: Props): React.ReactElement { const accounts = useContext(AccountContext); const [account, setAccount] = useState(null); const [chain, setChain] = useState(null); const [formatted, setFormatted] = useState(null); + const [showActionsMenu, setShowActionsMenu] = useState(false); + const actionsRef = useRef(null); + useOutsideClick(actionsRef, () => (showActionsMenu && setShowActionsMenu(!showActionsMenu))); useEffect((): void => { if (!address) { return; } - const [formatted, account, chain] = recodeAddress(address, accounts, genesisHash); setFormatted(formatted); @@ -69,36 +75,111 @@ function Address ({ address, children, className, genesisHash, name }: Props): R const theme = ((chain && chain.icon) || 'polkadot') as 'polkadot'; - return ( - - } - intro={ - <> -
{name || (account && account.name) || ''}
-
{formatted || ''}
- - } - > - {children} -
- ); + return
+ + + + {name || (account && account.name) || ''} + {formatted || ''} + + {actions && + <> + setShowActionsMenu(!showActionsMenu)} ref={actionsRef}> + {showActionsMenu ? : } + + {showActionsMenu && {actions}} + } + + {children} +
; } -export default styled(Address)` - .address { - opacity: 0.5; - overflow: hidden; - text-overflow: ellipsis; +const AccountInfoRow = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + height: 72px; + margin-bottom: 8px; + background: ${({ theme }): string => theme.btnAreaBackground}; +`; + +const Info = styled.div` + width: 100%; +`; + +const Name = styled.div` + margin: 2px 0; + font-weight: 600; + font-size: 16px; + line-height: 22px; +`; + +const FullAddress = styled.div` + width: 214px; + overflow: hidden; + text-overflow: ellipsis; + color: ${({ theme }): string => theme.labelColor}; + font-size: 12px; + line-height: 16px; +`; + +const Settings = styled.div` + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 40px; + + & ${Svg} { + width: 3px; + height: 19px; } + + &:before { + content: ""; + position: absolute; + left: 0; + top: 25%; + bottom: 25%; + width: 1px; + background: ${({ theme }): string => theme.inputBorder}; + } + + &:hover { + cursor: pointer; + background: ${({ theme }): string => theme.readonlyInputBackground}; + } +`; + +Settings.displayName = 'Details'; + +const ActionsIcon = styled(Svg).attrs(() => ({ + src: DetailsImg +}))` + background: ${({ theme }): string => theme.iconLabelColor}; +`; - .name { - padding-bottom: 0.5rem; +const ActiveActionsIcon = styled(Svg).attrs(() => ({ + src: DetailsImg +}))` + background: ${({ theme }): string => theme.primaryColor}; +`; + +export default styled(Address)` + position: relative; + + & ${Identicon} { + margin-left: 25px; + margin-right: 10px; + + & svg { + width: 50px; + height: 50px; + } } `; diff --git a/packages/extension-ui/src/components/IconBox.tsx b/packages/extension-ui/src/components/IconBox.tsx index 428778bf14c..d4eebdd2e41 100644 --- a/packages/extension-ui/src/components/IconBox.tsx +++ b/packages/extension-ui/src/components/IconBox.tsx @@ -20,6 +20,7 @@ interface Props { function IconBox ({ banner, children, className, icon, intro }: Props): React.ReactElement { return (
+
{icon}
{intro}
{children}
-
{icon}
); } @@ -36,26 +36,17 @@ export default styled(IconBox)` box-sizing: border-box; margin: ${({ theme }): string => theme.boxMargin}; padding: ${({ theme }): string => theme.boxPadding}; - padding-left: 1rem; - position: relative; .details { margin: 0; - - .intro { - padding-left: 3rem; - } } .outer-icon { height: 64px; + width: 64px; font-size: 36px; - left: 0.25rem; line-height: 64px; - position: absolute; - top: -0.5rem; vertical-align: middle; - width: 64px; z-index: 1; } `; diff --git a/packages/extension-ui/src/components/Identicon.tsx b/packages/extension-ui/src/components/Identicon.tsx index bbcef13cc3f..6a3f414af04 100644 --- a/packages/extension-ui/src/components/Identicon.tsx +++ b/packages/extension-ui/src/components/Identicon.tsx @@ -24,6 +24,9 @@ function Identicon ({ iconTheme, className, value }: Props): React.ReactElement< } export default styled(Identicon)` + display: flex; + justify-content: center; + .container:before { box-shadow: none; background: ${({ theme }): string => theme.identiconBackground}; diff --git a/packages/extension-ui/src/components/Menu.tsx b/packages/extension-ui/src/components/Menu.tsx new file mode 100644 index 00000000000..9ac0a86f701 --- /dev/null +++ b/packages/extension-ui/src/components/Menu.tsx @@ -0,0 +1,31 @@ +// Copyright 2019 @polkadot/extension-ui authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import React from 'react'; +import styled from 'styled-components'; + +interface Props { + children: React.ReactNode; + className?: string; +} + +function Menu ({ children, className }: Props): React.ReactElement { + return
+ {children} +
; +} + +export default styled(Menu)` + position: absolute; + right: 0; + margin-top: 90px; + padding: 16px 0; + background: ${({ theme }): string => theme.btnAreaBackground}; + border-radius: 4px; + border: 1px solid #222222; + box-sizing: border-box; + box-shadow: 0 0 32px rgba(0, 0, 0, 0.86); + backdrop-filter: blur(10px); + z-index: 1; +`; diff --git a/packages/extension-ui/src/components/Svg.tsx b/packages/extension-ui/src/components/Svg.tsx new file mode 100644 index 00000000000..e84c50f3bcb --- /dev/null +++ b/packages/extension-ui/src/components/Svg.tsx @@ -0,0 +1,16 @@ +// Copyright 2019 @polkadot/extension-ui authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import styled from 'styled-components'; + +interface Props { + src: string; +} + +export default styled.span` + display: block; + mask: ${({ src }): string => `url(${src})`}; + mask-size: cover; + background: ${({ theme }): string => theme.color}; +`; diff --git a/packages/extension-ui/src/components/Warning.tsx b/packages/extension-ui/src/components/Warning.tsx index 42819f337a5..70b4628b39b 100644 --- a/packages/extension-ui/src/components/Warning.tsx +++ b/packages/extension-ui/src/components/Warning.tsx @@ -5,6 +5,7 @@ import React from 'react'; import styled from 'styled-components'; import WarningImageSrc from '../assets/warning.svg'; +import Svg from '@polkadot/extension-ui/components/Svg'; interface Props { children: React.ReactNode; @@ -12,25 +13,22 @@ interface Props { className?: string; } -const WarningImage = styled.span>` - display: inline-block; - width: 25px; - height: 14px; - margin: 5px 16px 5px 0; - mask: url(${WarningImageSrc}); - mask-size: cover; - background: ${({ danger, theme }): string => danger ? theme.iconDangerColor : theme.iconWarningColor}; -`; - function Warning ({ children, className, danger }: Props): React.ReactElement { return
- +
{children}
; } +const WarningImage = styled(Svg)>` + width: 25px; + height: 14px; + margin: 5px 16px 5px 0; + background: ${({ danger, theme }): string => danger ? theme.iconDangerColor : theme.iconWarningColor}; +`; + export default styled(Warning)` display: flex; flex-direction: row; diff --git a/packages/extension-ui/src/components/index.ts b/packages/extension-ui/src/components/index.ts index 796407c489d..f2fd8a155ef 100644 --- a/packages/extension-ui/src/components/index.ts +++ b/packages/extension-ui/src/components/index.ts @@ -20,7 +20,9 @@ export { default as InputWithLabel } from './InputWithLabel'; export { default as Link } from './Link'; export { default as List } from './List'; export { default as Loading } from './Loading'; +export { default as Menu } from './Menu'; export { default as MnemonicSeed } from './MnemonicSeed'; +export { default as Svg } from './Svg'; export { default as TextAreaWithLabel } from './TextAreaWithLabel'; export { default as Tip } from './Tip'; export { default as VerticalSpace } from './VerticalSpace'; diff --git a/packages/extension-ui/src/components/themes.ts b/packages/extension-ui/src/components/themes.ts index 451f8c04ae8..4b734186dc0 100644 --- a/packages/extension-ui/src/components/themes.ts +++ b/packages/extension-ui/src/components/themes.ts @@ -2,9 +2,9 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -const DANGER_COLOR = '#D92A2A'; +const BUTTON_DANGER_COLOR = '#D92A2A'; +const TEXT_DANGER_COLOR = '#FF5858'; const LABEL_COLOR = '#9F9E99'; -const LINK_COLOR = '#9F9E99'; const TEXT_COLOR = '#FFFFFF'; const BG_COLOR = 'rgba(13, 14, 19, 0.9)'; @@ -13,7 +13,7 @@ export const defaultTheme = { borderRadius: '4px', btnAreaBackground: 'rgba(13, 14, 19, 0.7)', btnBg: 'linear-gradient(95.52deg, #FF8A00 0.14%, #FF7A00 100.14%)', - btnBgDanger: DANGER_COLOR, + btnBgDanger: BUTTON_DANGER_COLOR, btnBorder: '0 solid ', btnColor: TEXT_COLOR, btnColorDanger: TEXT_COLOR, @@ -30,7 +30,8 @@ export const defaultTheme = { inputBackground: 'rgba(13, 14, 19, 0.9)', readonlyInputBackground: '#000000', iconWarningColor: '#FF7D01', - iconDangerColor: '#FF5858', + iconDangerColor: TEXT_DANGER_COLOR, + iconLabelColor: '#8E8E8E', identiconBackground: '#373737', inputBorder: '#303030', inputHeight: '40px', @@ -39,13 +40,13 @@ export const defaultTheme = { labelFontSize: '13px', labelLineHeight: '18px', lineHeight: '26px', - linkColor: LINK_COLOR, - linkColorDanger: DANGER_COLOR, + linkColor: TEXT_COLOR, + linkColorDanger: TEXT_DANGER_COLOR, primaryColor: '#FF7D01', box: { error: { background: '#ffe6e6', - border: DANGER_COLOR, + border: BUTTON_DANGER_COLOR, color: '#4d0000' }, info: { diff --git a/packages/extension-ui/src/hooks.ts b/packages/extension-ui/src/hooks.ts new file mode 100644 index 00000000000..c2f766577ec --- /dev/null +++ b/packages/extension-ui/src/hooks.ts @@ -0,0 +1,19 @@ +// Copyright 2019 @polkadot/extension-ui authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { useEffect, RefObject } from 'react'; + +export const useOutsideClick = (ref: RefObject, callback: () => void): void => { + const handleClick = (e: MouseEvent): void => { + if (ref.current && !ref.current.contains(e.target as HTMLInputElement)) { + callback(); + } + }; + useEffect(() => { + document.addEventListener('click', handleClick); + return (): void => { + document.removeEventListener('click', handleClick); + }; + }); +};