Nav re-imagined (#3422)

* Playthrough with top navigation

* Sycky, but navigation works...

* Adjust

* grouping

* It works, but is not great

* Cleanups

* Cleanups

* hover color

* Highlights

* Highlights, part 2

* Background to body

* Small adjustments (size, items)

* Localize Modal/route switching

* Remove light/full mode

* Flatter single-entry menus

* Adjustments for single-enrties (e.g. Kulupu)

* Display NodeInfo

* Misc

* Content & Tabs sizing

* Tab counters as bubbles

* Alignment

* Tabs styling

* Adjust font color to defaults

* Small tabs menu color adjustments

* ... the previous, again, adjusted

* Display Wiki/Github links

* ar motions

* Adjust Endpoints location

* Ensure content grows to available height

* Adjust tabs

* Menu background adjustment

* Adjust popup colors

* Adjust highlights

* Sligh breathing room

* Adaptive header background

* 2/3

* Adaptive is ok... for now.

* Back to black... erm, gray

* Lighten

* Touchups. Stop playin, it is ok...

* Sides readability

* Cleanups.

* Adjust.

* Overall adjustments (theme). Fix Tag yellow.

* Ok. Done.

* Ship it.
This commit is contained in:
Jaco Greeff 2020-08-20 17:50:30 +02:00 committed by GitHub
parent 1fb364b741
commit 33892a2e31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 568 additions and 761 deletions

View File

@ -12,6 +12,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
display: {
needsApi: []
},
group: 'accounts',
icon: 'users',
name: 'accounts',
text: t<string>('nav.accounts', 'Accounts', { ns: 'apps-routing' }),

View File

@ -15,6 +15,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
'query.claims.claims'
]
},
group: 'accounts',
icon: 'star',
name: 'claims',
text: t<string>('nav.claims', 'Claim Tokens', { ns: 'apps-routing' }),

View File

@ -15,6 +15,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
'tx.contracts.call'
]
},
group: 'developer',
icon: 'compress',
name: 'contracts',
text: t<string>('nav.contracts', 'Contracts', { ns: 'apps-routing' })

View File

@ -17,6 +17,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
]
]
},
group: 'governance',
icon: 'building',
name: 'council',
text: t<string>('nav.council', 'Council', { ns: 'apps-routing' }),

View File

@ -12,6 +12,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
display: {
isHidden: true
},
group: 'network',
icon: 'th',
name: 'dashboard',
text: t<string>('nav.dashboard', 'Dashboard', { ns: 'apps-routing' })

View File

@ -14,6 +14,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
'tx.democracy.notePreimage'
]
},
group: 'governance',
icon: 'calendar-check',
name: 'democracy',
text: t<string>('nav.democracy', 'Democracy', { ns: 'apps-routing' }),

View File

@ -12,6 +12,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
display: {
needsApi: []
},
group: 'network',
icon: 'braille',
name: 'explorer',
text: t<string>('nav.explorer', 'Explorer', { ns: 'apps-routing' })

View File

@ -13,6 +13,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
needsAccounts: true,
needsApi: []
},
group: 'developer',
icon: 'sync',
name: 'extrinsics',
text: t<string>('nav.extrinsics', 'Extrinsics', { ns: 'apps-routing' })

View File

@ -16,6 +16,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
'tx.genericAsset.transfer'
]
},
group: 'network',
icon: 'cubes',
name: 'generic-asset',
text: t<string>('nav.generic-asset', 'Generic asset', { ns: 'apps-routing' })

View File

@ -4,10 +4,6 @@
import { Routes } from './types';
import appSettings from '@polkadot/ui-settings';
// When adding here, also ensure to add to Dummy.tsx
import accounts from './accounts';
import claims from './claims';
import contracts from './contracts';
@ -31,47 +27,27 @@ import transfer from './transfer';
import treasury from './treasury';
export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Routes {
return appSettings.uiMode === 'light'
? [
// dashboard,
explorer(t),
accounts(t),
claims(t),
poll(t),
transfer(t),
genericAsset(t),
null,
staking(t),
democracy(t),
council(t),
// TODO Not sure about the inclusion of treasury, parachains & society here
null,
settings(t)
]
: [
// dashboard(t),
explorer(t),
accounts(t),
claims(t),
poll(t),
transfer(t),
genericAsset(t),
null,
staking(t),
democracy(t),
council(t),
treasury(t),
techcomm(t),
parachains(t),
society(t),
null,
contracts(t),
storage(t),
extrinsics(t),
sudo(t),
null,
settings(t),
toolbox(t),
js(t)
];
return [
// dashboard(t),
accounts(t),
explorer(t),
claims(t),
poll(t),
transfer(t),
genericAsset(t),
staking(t),
democracy(t),
council(t),
treasury(t),
techcomm(t),
parachains(t),
society(t),
contracts(t),
storage(t),
extrinsics(t),
sudo(t),
settings(t),
toolbox(t),
js(t)
];
}

View File

@ -12,6 +12,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
display: {
needsApi: []
},
group: 'developer',
icon: 'code',
name: 'js',
text: t<string>('nav.js', 'Javascript', { ns: 'apps-routing' })

View File

@ -14,6 +14,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
['query.parachains.code', 'query.parachainUpgrade.didUpdateVFPs']
]
},
group: 'network',
icon: 'link',
name: 'parachains',
text: t<string>('nav.parachains', 'Parachains', { ns: 'apps-routing' })

View File

@ -15,6 +15,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
'tx.poll.vote'
]
},
group: 'governance',
icon: 'podcast',
name: 'poll',
text: t<string>('nav.poll', 'Token poll', { ns: 'apps-routing' })

View File

@ -10,6 +10,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
return {
Component,
display: {},
group: 'settings',
icon: 'cogs',
name: 'settings',
text: t<string>('nav.settings', 'Settings', { ns: 'apps-routing' }),

View File

@ -15,6 +15,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
'query.society.pot'
]
},
group: 'network',
icon: 'hand-spock',
name: 'society',
text: t<string>('nav.society', 'Society', { ns: 'apps-routing' }),

View File

@ -14,6 +14,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
['tx.staking.bond']
]
},
group: 'network',
icon: 'certificate',
name: 'staking',
text: t<string>('nav.staking', 'Staking', { ns: 'apps-routing' })

View File

@ -12,6 +12,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
display: {
needsApi: []
},
group: 'developer',
icon: 'database',
name: 'chainstate',
text: t<string>('nav.storage', 'Chain state', { ns: 'apps-routing' })

View File

@ -16,6 +16,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
],
needsSudo: true
},
group: 'developer',
icon: 'unlock',
name: 'sudo',
text: t<string>('nav.sudo', 'Sudo', { ns: 'apps-routing' })

View File

@ -15,6 +15,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
'query.technicalCommittee.members'
]
},
group: 'governance',
icon: 'microchip',
name: 'techcomm',
text: t<string>('nav.tech-comm', 'Tech. comm.', { ns: 'apps-routing' }),

View File

@ -12,6 +12,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
display: {
needsApi: []
},
group: 'developer',
icon: 'tools',
name: 'toolbox',
text: t<string>('nav.toolbox', 'Toolbox', { ns: 'apps-routing' })

View File

@ -17,6 +17,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
'tx.balances.transfer'
]
},
group: 'accounts',
icon: 'paper-plane',
name: 'transfer',
text: t<string>('nav.transfer', 'Transfer', { ns: 'apps-routing' })

View File

@ -14,6 +14,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
'tx.treasury.proposeSpend'
]
},
group: 'governance',
icon: 'gem',
name: 'treasury',
text: t<string>('nav.treasury', 'Treasury', { ns: 'apps-routing' }),

View File

@ -5,6 +5,8 @@
import { IconName } from '@fortawesome/fontawesome-svg-core';
import { AppProps, BareProps } from '@polkadot/react-components/types';
export type RouteGroup = 'accounts' | 'developer' | 'governance' | 'network' | 'settings';
export interface RouteProps extends AppProps, BareProps {
location: any;
}
@ -19,6 +21,7 @@ export interface Route {
needsApi?: (string | string[])[];
needsSudo?: boolean;
};
group: RouteGroup;
icon: IconName;
isIgnored?: boolean;
name: string;
@ -26,4 +29,4 @@ export interface Route {
useCounter?: () => number | string | null;
}
export type Routes = (Route | null)[];
export type Routes = Route[];

View File

@ -248,7 +248,7 @@
"Mnemonic": "ذاكري",
"Most profitable": "الأكثر ربحية",
"Most recent head data": "أحدث بيانات الرأس",
"Motions ({{count}})": "({{الحركات ({{احسب",
"Motions": "الحركات",
"Multiple council proposals could exist, both approval and rejection. Apply your vote to the correct council proposal (also available on council motions page)": "(قد تنشأ مقترحات متعددة من جانب المجلس، سواء بالموافقة أو الرفض. طبق تصويتك على اقتراح المجلس الصحيح (متاح أيضا في صفحة طلبات المجلس",
"Multisig": "متعدد الدخول",
"Multisig approval with hash (non-final approval)": "(اعتماد متعدد التوقيعات مع تجزئة (موافقة غير نهائية",
@ -1277,4 +1277,4 @@
"{{threshold}}, not passing": "سقف}}، لا يتم تمريره}}",
"{{threshold}}, passing": "سقف}}، يتم تمريره}}",
"{{value}}x voting balance, locked for {{lock}}x enactment ({{period}} days)": "({{قفل}} (تاريخ إصدار {{أيام}}x رصيد التصويت، مقفل لـ x {{قيمة }}"
}
}

View File

@ -6,7 +6,7 @@
"Council overview": "Council overview",
"Current prime member, default voting": "Current prime member, default voting",
"In calculating the election outcome, this prioritized vote ordering will be used to determine the final score for the candidates.": "In calculating the election outcome, this prioritized vote ordering will be used to determine the final score for the candidates.",
"Motions ({{count}})": "Motions ({{count}})",
"Motions": "Motions",
"Nay {{counter}}": "Nay {{counter}}",
"No candidates found": "No candidates found",
"No council motions": "No council motions",
@ -77,4 +77,4 @@
"voting account": "voting account",
"voting end": "voting end",
"voting round": "voting round"
}
}

View File

@ -221,7 +221,7 @@
"Metadata {{count}}": "",
"Mnemonic": "",
"Most recent head data": "",
"Motions ({{count}})": "",
"Motions": "",
"Multiple council proposals could exist, both approval and rejection. Apply your vote to the correct council proposal (also available on council motions page)": "",
"Multisig": "",
"Multisig approval with hash (non-final approval)": "",
@ -1152,4 +1152,4 @@
"{{threshold}}, not passing": "",
"{{threshold}}, passing": "",
"{{value}}x voting balance, locked for {{lock}}x enactment ({{period}} days)": ""
}
}

View File

@ -205,7 +205,7 @@
"Metadata {{count}}": "Metadatos {{count}}",
"Mnemonic": "Mnemonic",
"Most recent head data": "Datos más recientes de la cabecera",
"Motions ({{count}})": "Mociones ({{count}})",
"Motions": "Mociones",
"Multiple council proposals could exist, both approval and rejection. Apply your vote to the correct council proposal (also available on council motions page)": "Podrían existir múltiples propuestas del consejo, tanto de aprobación como de rechazo. Aplique su voto a la propuesta correcta del consejo (también disponible en la página de mociones del consejo)",
"Multisig": "Multisig",
"Multisig approval with hash (non-final approval)": "Aprobación de Multisig con hash (aprobación no definitiva)",
@ -1113,4 +1113,4 @@
"{{threshold}}, not passing": "{{threshold}}, no pasará",
"{{threshold}}, passing": "{{threshold}}, de aprobación",
"{{value}}x voting balance, locked for {{lock}}x enactment ({{period}} days)": "{{value}}x saldo de votos, bloqueado por {{lock}}x sentencia ({{period}} días."
}
}

View File

@ -140,7 +140,7 @@
"Metadata {{count}}": "メタデータ {{count}}",
"Mnemonic": "ニーモニック",
"Most recent head data": "最新のヘッドデータ",
"Motions ({{count}})": "モーション({{count}})",
"Motions": "モーション",
"Multisig": "マルチシグ",
"Multisig approvals": "マルチシグの承認",
"Mutates contract state": "コントラクトの状態を変える",
@ -562,4 +562,4 @@
"{{relayDispatchQueueSize}} dispatch messages pending": "{{relayDispatchQueueSize}} ディスパッチメッセージペンディング",
"{{step}}. Sign with your ETH address": "{{step}}. ETHアドレスでサインしてください",
"{{s}} s": "{{s}} 秒"
}
}

View File

@ -205,7 +205,7 @@
"Metadata {{count}}": "Metadados {{count}}",
"Mnemonic": "Mnemônico",
"Most recent head data": "Dados de cabeçalho mais recentes",
"Motions ({{count}})": "Moções ({{count}})",
"Motions": "Moções",
"Multiple council proposals could exist, both approval and rejection. Apply your vote to the correct council proposal (also available on council motions page)": "Múltiplas propostas de conselho podem existir, tanto aprovação quanto rejeição. Aplique seu voto à proposta correta do conselho (também disponível na página de moções do conselho)",
"Multisig": "Multi-assinatura",
"Multisig approval with hash (non-final approval)": "Aprovação multi-assinatura com hash (aprovação não final)",
@ -1120,4 +1120,4 @@
"{{threshold}}, not passing": "{{threshold}}, não passando",
"{{threshold}}, passing": "{{threshold}}, passando",
"{{value}}x voting balance, locked for {{lock}}x enactment ({{period}} days)": "{{value}} x saldo de votos, bloqueado por {{lock}} x promulgação ({{period}} dias)"
}
}

View File

@ -210,7 +210,7 @@
"Mnemonic": "Мнемонический",
"More": "Ещё",
"Most recent head data": "Последние данные",
"Motions ({{count}})": "Ходатайства ({{count}})",
"Motions": "Ходатайства",
"Multiple council proposals could exist, both approval and rejection. Apply your vote to the correct council proposal (also available on council motions page)": "Нескольо предложний могут существовать, как одобренных так и отклоненных. Голосуйте в правильном предложении (это можно сделать и на странице Решения Совета)",
"Multisig": "Мультиподпись",
"Multisig approval with hash (non-final approval)": "Разрешить с хэшом мультиподписи (не финальное разрешение)",
@ -1131,4 +1131,4 @@
"{{threshold}}, not passing": "{{threshold}}, не проходит",
"{{threshold}}, passing": "{{threshold}}, проходит",
"{{value}}x voting balance, locked for {{lock}}x enactment ({{period}} days)": "{{value}}x баланс голосования, заблокировано на {{lock}}x ({{period}} дней)"
}
}

View File

@ -123,7 +123,7 @@
"Manage your connection to Ledger S": "管理你的连接到 Ledger S",
"Messages": "消息",
"Mnemonic": "助记词",
"Motions ({{count}})": "Motions ({{count}})",
"Motions": "Motions",
"Multisig": "多重签名",
"Multisig approvals": "多重签名批准",
"My accounts": "我的账户",
@ -561,4 +561,4 @@
"yes": "是",
"your current password": "你当前的密码",
"your new password": "你的新密码"
}
}

View File

@ -4,8 +4,7 @@
import { BareProps as Props } from '@polkadot/react-components/types';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import store from 'store';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import AccountSidebar from '@polkadot/app-accounts/Sidebar';
import { getSystemChainColor } from '@polkadot/apps-config/ui';
@ -15,86 +14,26 @@ import { useApi } from '@polkadot/react-hooks';
import Signer from '@polkadot/react-signer';
import ConnectingOverlay from './overlays/Connecting';
import { SideBarTransition, SIDEBAR_MENU_THRESHOLD } from './constants';
import Content from './Content';
import SideBar from './SideBar';
import Menu from './Menu';
import WarmUp from './WarmUp';
import { WindowDimensionsCtx } from './WindowDimensions';
interface SidebarState {
isCollapsed: boolean;
isMenu: boolean;
isMenuOpen: boolean;
transition: SideBarTransition;
}
export const PORTAL_ID = 'portals';
function saveSidebar (sidebar: SidebarState): SidebarState {
return store.set('sidebar', sidebar) as SidebarState;
}
function Apps ({ className = '' }: Props): React.ReactElement<Props> {
const windowDimensions = useContext(WindowDimensionsCtx);
const { systemChain, systemName } = useApi();
const [sidebar, setSidebar] = useState<SidebarState>({
isCollapsed: false,
isMenuOpen: false,
transition: SideBarTransition.COLLAPSED,
...store.get('sidebar', {}),
isMenu: window.innerWidth < SIDEBAR_MENU_THRESHOLD
});
const uiHighlight = useMemo(
() => getSystemChainColor(systemChain, systemName),
[systemChain, systemName]
);
const _collapse = useCallback(
() => setSidebar((sidebar: SidebarState) => saveSidebar({ ...sidebar, isCollapsed: !sidebar.isCollapsed })),
[]
);
const _toggleMenu = useCallback(
() => setSidebar((sidebar: SidebarState) => saveSidebar({ ...sidebar, isCollapsed: false, isMenuOpen: true })),
[]
);
const _handleResize = useCallback(
(): void => {
const transition = window.innerWidth < SIDEBAR_MENU_THRESHOLD
? SideBarTransition.MINIMISED_AND_EXPANDED
: SideBarTransition.EXPANDED_AND_MAXIMISED;
setSidebar((sidebar: SidebarState) => saveSidebar({
...sidebar,
isMenu: transition === SideBarTransition.MINIMISED_AND_EXPANDED,
isMenuOpen: false,
transition
}));
},
[]
);
useEffect((): void => {
_handleResize();
}, [_handleResize, windowDimensions]);
const { isCollapsed, isMenu, isMenuOpen } = sidebar;
return (
<>
<GlobalStyle uiHighlight={defaultColor || uiHighlight} />
<div className={`apps--Wrapper ${isCollapsed ? 'collapsed' : 'expanded'}${isMenu ? ' fixed' : ''}${isMenuOpen ? ' menu-open' : ''} theme--default ${className}`}>
<div className={`apps--Wrapper theme--default ${className}`}>
<Menu />
<AccountSidebar>
<div
className={`apps--Menu-bg ${isMenuOpen ? 'open' : 'closed'}`}
onClick={_handleResize}
/>
<SideBar
collapse={_collapse}
handleResize={_handleResize}
isCollapsed={isCollapsed}
isMenuOpen={isMenuOpen}
toggleMenu={_toggleMenu}
/>
<Signer>
<Content />
</Signer>
@ -110,116 +49,6 @@ function Apps ({ className = '' }: Props): React.ReactElement<Props> {
export default React.memo(styled(Apps)`
box-sizing: border-box;
display: flex;
flex-direction: row;
height: 100vh;
&.theme--default {
a.apps--SideBar-Item-NavLink {
color: #f5f4f3;
display: block;
padding: 0.75em 0.75em;
white-space: nowrap;
&:hover {
background: #5f5f5f;
border-radius: 0.28571429rem 0 0 0.28571429rem;
color: #eee;
margin-right: 0.25rem;
}
}
a.apps--SideBar-Item-NavLink-active {
background: #f5f4f3;
border-radius: 0.28571429rem 0 0 0.28571429rem;
/* border-bottom: 2px solid transparent; */
color: #3f3f3f;
&:hover {
background: #f5f4f3;
color: #3f3f3f;
margin-right: 0;
}
}
}
&.collapsed .apps--SideBar {
text-align: center;
.divider {
display: none;
}
.apps--SideBar-Item {
margin-left: 5px;
.text {
display: none;
}
}
.apps--SideBar-logo {
.apps--SideBar-logo-inner {
margin: auto;
padding: 0;
width: 3rem;
img {
margin: 0 0.4rem;
}
> div.info {
display: none;
}
}
}
.apps--SideBar-collapse .ui.basic.secondary.button {
left: 0.66rem;
}
}
&.expanded .apps--SideBar {
text-align: left;
.apps--SideBar-Scroll {
padding-left: 0.75rem;
}
}
&.fixed {
.apps--SideBar-Wrapper {
position: absolute;
width: 0px;
.apps--SideBar {
padding-left: 0;
}
}
}
&.menu-open {
.apps--SideBar-Wrapper {
width: 12rem;
}
}
.apps--Menu-bg {
background: transparent;
height: 100%;
left: 0;
position: absolute;
top: 0;
transition: opacity 0.2s;
width: 100%;
z-index: 299;
&.closed {
opacity: 0;
width: 0;
}
&.open {
opacity: 1;
}
}
flex-direction: column;
min-height: 100vh;
`);

View File

@ -25,6 +25,7 @@ const NOT_FOUND: Route = {
display: {
needsApi: undefined
},
group: 'settings',
icon: 'times',
isIgnored: false,
name: 'unknown',
@ -78,13 +79,10 @@ function Content ({ className }: Props): React.ReactElement<Props> {
}
export default React.memo(styled(Content)`
background: #f5f4f3;
flex-grow: 1;
height: 100%;
min-height: 100vh;
overflow-x: hidden;
overflow-y: auto;
padding: 0 1.5rem;
padding: 0 2.5rem 1rem;
position: relative;
width: 100%;

View File

@ -7,73 +7,80 @@ import { RuntimeVersion } from '@polkadot/types/interfaces';
import React from 'react';
import styled from 'styled-components';
import { ChainImg, Icon } from '@polkadot/react-components';
import { useCall, useApi } from '@polkadot/react-hooks';
import { useApi, useCall, useIpfs, useToggle } from '@polkadot/react-hooks';
import { BestNumber, Chain } from '@polkadot/react-query';
import Endpoints from '../Endpoints';
import { useTranslation } from '../translate';
interface Props {
className?: string;
isToggled?: boolean;
onClick?: () => void;
}
function ChainInfo ({ className = '', isToggled, onClick }: Props): React.ReactElement<Props> {
function ChainInfo ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const runtimeVersion = useCall<RuntimeVersion>(api.rpc.state.subscribeRuntimeVersion);
const { ipnsChain } = useIpfs();
const [isEndpointsVisible, toggleEndpoints] = useToggle();
const canToggle = !ipnsChain;
return (
<div
className={`apps--SideBar-logo${onClick ? ' isClickable' : ''} ${className} ui--highlight--border`}
onClick={onClick}
>
<div className='apps--SideBar-logo-inner'>
<div className={className}>
<div
className={`apps--SideBar-logo-inner${canToggle ? ' isClickable' : ''}`}
onClick={toggleEndpoints}
>
<ChainImg />
<div className='info'>
<Chain className='chain' />
{runtimeVersion && (
<div className='runtimeVersion'>{t<string>('version {{version}}', { replace: { version: runtimeVersion.specVersion.toNumber() } })}</div>
)}
<BestNumber label='#' />
<BestNumber
className='bestNumber'
label='#'
/>
</div>
{onClick && (
{canToggle && (
<Icon
className='dropdown'
icon={isToggled ? 'caret-right' : 'caret-down'}
icon={isEndpointsVisible ? 'caret-right' : 'caret-down'}
/>
)}
</div>
{isEndpointsVisible && (
<Endpoints onClose={toggleEndpoints} />
)}
</div>
);
}
export default React.memo(styled(ChainInfo)`
border-top: 0.5rem solid transparent;
box-sizing: border-box;
padding: 0.75rem;
margin: 0 0 0.5rem -1rem;
&.isClickable {
cursor: pointer;
}
padding: 0.75rem 1rem 0.75rem 1.5rem;
margin: 0;
.apps--SideBar-logo-inner {
display: flex;
align-items: center;
justify-content: space-between;
width: 10.5rem;
&.isClickable {
cursor: pointer;
}
img {
flex: 0;
height: 2.75rem;
width: 2.75rem;
height: 3rem;
margin-right: 0.5rem;
width: 3rem;
}
.ui--Icon.dropdown,
> div.info {
color: white;
opacity: 0.75;
opacity: 0.85;
text-align: right;
vertical-align: middle;
}
@ -88,14 +95,15 @@ export default React.memo(styled(ChainInfo)`
flex: 1;
padding-right: 0.5rem;
> div.bestNumber,
> div.chain {
font-size: 0.9rem;
line-height: 1rem;
line-height: 1.2;
}
> div.runtimeVersion {
font-size: 0.75rem;
line-height: 1rem;
line-height: 1.2;
}
}
}

View File

@ -0,0 +1,91 @@
// Copyright 2017-2020 @polkadot/apps 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 { Group } from './types';
import React from 'react';
import styled from 'styled-components';
import { Icon } from '@polkadot/react-components';
import Item from './Item';
interface Props extends Group {
className?: string;
}
function Grouping ({ className, name, routes }: Props): React.ReactElement<Props> {
if (routes.length === 1) {
return (
<Item
className='topLevel'
route={routes[0]}
/>
);
}
return (
<li className={className}>
<div className='groupHdr'>
<span>{name}</span>
<Icon icon='caret-down' />
</div>
<ul className='groupMenu'>
{routes.map((route): React.ReactNode => (
<Item
key={route.name}
route={route}
/>
))}
</ul>
</li>
);
}
export default React.memo(styled(Grouping)`
cursor: pointer;
position: relative;
.groupHdr {
border-radius: 0.25rem 0.25rem 0 0;
padding: 1rem 1.5rem;
> .ui--Icon {
margin-left: 0.75rem;
}
}
.groupMenu {
background: #6a6662;
border-radius: 0 0 0.25rem 0.25rem;
display: none;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
z-index: 250;
> li {
z-index: 1;
a {
padding-right: 3.5rem;
}
}
}
&:hover {
.groupHdr,
.groupMenu li {
background: #74706c;
}
.groupMenu {
display: block;
> li:hover {
background: transparent;
}
}
}
`);

View File

@ -0,0 +1,80 @@
// Copyright 2017-2020 @polkadot/apps 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 { ItemRoute } from './types';
import React from 'react';
import styled from 'styled-components';
import { Badge, Icon } from '@polkadot/react-components';
import { useToggle } from '@polkadot/react-hooks';
interface Props {
className?: string;
route: ItemRoute;
}
const DUMMY_COUNTER = () => 0;
function Item ({ className = '', route: { Modal, href, icon, name, text, useCounter = DUMMY_COUNTER } }: Props): React.ReactElement<Props> {
const [isModalVisible, toggleModal] = useToggle();
const count = useCounter();
return (
<li className={`${className}${count ? ' withCounter' : ''}`}>
<a
href={Modal ? undefined : (href || `#/${name}`)}
onClick={Modal ? toggleModal : undefined}
rel='noopener noreferrer'
target={href ? '_blank' : undefined}
>
<Icon icon={icon} />
{text}
{!!count && (
<Badge
color='counter'
info={count}
/>
)}
</a>
{Modal && isModalVisible && (
<Modal onClose={toggleModal} />
)}
</li>
);
}
export default React.memo(styled(Item)`
cursor: pointer;
position: relative;
white-space: nowrap;
&.topLevel {
padding: 0;
a {
padding: 1rem 1.5rem;
}
}
&.withCounter a {
padding-right: 3.5rem;
}
a {
color: inherit !important;
display: block;
padding: 0.75rem 1.5rem;
text-decoration: none;
}
.ui--Badge {
position: absolute;
right: 0.25rem;
top: 0.65rem;
}
.ui--Icon {
margin-right: 0.5rem;
}
`);

View File

@ -14,11 +14,11 @@ const pkgJson = require('../../package.json') as { version: string };
const uiInfo = `apps v${pkgJson.version}`;
function NodeInfo ({ className }: Props): React.ReactElement<Props> {
function NodeInfo ({ className = '' }: Props): React.ReactElement<Props> {
const { api, isApiReady } = useApi();
return (
<div className={className}>
<div className={`${className} ui--media-1400`}>
{isApiReady && (
<div>
<NodeName />&nbsp;
@ -34,9 +34,10 @@ function NodeInfo ({ className }: Props): React.ReactElement<Props> {
export default React.memo(styled(NodeInfo)`
background: transparent;
color: white;
font-size: 0.75rem;
opacity: 0.5;
padding: 0 1.5rem 0 1.5rem;
font-size: 0.9rem;
line-height: 1.2;
opacity: 0.85;
padding: 0 1.5rem;
text-align: right;
> div {

View File

@ -0,0 +1,205 @@
// Copyright 2017-2020 @polkadot/apps 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 { Route, Routes } from '@polkadot/apps-routing/types';
import { ApiProps } from '@polkadot/react-api/types';
import { AccountId } from '@polkadot/types/interfaces';
import { Group, Groups, ItemRoute } from './types';
import { TFunction } from 'i18next';
import React, { useMemo, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import styled from 'styled-components';
import createRoutes from '@polkadot/apps-routing';
import { Icon } from '@polkadot/react-components';
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
import { findMissingApis } from '../endpoint';
import { useTranslation } from '../translate';
import ChainInfo from './ChainInfo';
import Grouping from './Grouping';
import Item from './Item';
import NodeInfo from './NodeInfo';
interface Props {
className?: string;
}
const disabledLog = new Map<string, string>();
function createExternals (t: TFunction): ItemRoute[] {
return [
{ href: 'https://github.com/polkadot-js/apps', icon: 'code-branch', name: 'github', text: t<string>('nav.github', 'GitHub', { ns: 'apps-routing' }) },
{ href: 'https://wiki.polkadot.network', icon: 'book', name: 'wiki', text: t<string>('nav.wiki', 'Wiki', { ns: 'apps-routing' }) }
];
}
function logDisabled (route: string, message: string): void {
if (!disabledLog.get(route)) {
disabledLog.set(route, message);
console.warn(`Disabling ${route}: ${message}`);
}
}
function checkVisible (name: string, { api, isApiConnected, isApiReady }: ApiProps, hasAccounts: boolean, hasSudo: boolean, { isHidden, needsAccounts, needsApi, needsSudo }: Route['display']): boolean {
if (isHidden) {
return false;
} else if (needsAccounts && !hasAccounts) {
return false;
} else if (!needsApi) {
return true;
} else if (!isApiReady || !isApiConnected) {
return false;
} else if (needsSudo && !hasSudo) {
logDisabled(name, 'Sudo key not available');
return false;
}
const notFound = findMissingApis(api, needsApi);
if (notFound.length !== 0) {
logDisabled(name, `API not available: ${notFound.toString()}`);
}
return notFound.length === 0;
}
function extractGroups (routing: Routes, groupNames: Record<string, string>, apiProps: ApiProps, hasAccounts: boolean, hasSudo: boolean): Group[] {
return Object
.values(
routing.reduce((all: Groups, route): Groups => {
if (!all[route.group]) {
all[route.group] = { name: groupNames[route.group], routes: [route] };
} else {
all[route.group].routes.push(route);
}
return all;
}, {})
)
.map(({ name, routes }): Group => ({
name,
routes: routes.filter(({ display, name }) => checkVisible(name, apiProps, hasAccounts, hasSudo, display))
}))
.filter(({ routes }) => routes.length);
}
function Menu ({ className = '' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { allAccounts, hasAccounts } = useAccounts();
const apiProps = useApi();
const sudoKey = useCall<AccountId>(apiProps.isApiReady && apiProps.api.query.sudo?.key);
const location = useLocation();
const externalRef = useRef(createExternals(t));
const groupRef = useRef({
accounts: t('Accounts'),
developer: t('Developer'),
governance: t('Governance'),
network: t('Network'),
settings: t('Settings')
});
const routeRef = useRef(createRoutes(t));
const hasSudo = useMemo(
() => !!sudoKey && allAccounts.some((address) => sudoKey.eq(address)),
[allAccounts, sudoKey]
);
const visibleGroups = useMemo(
() => extractGroups(routeRef.current, groupRef.current, apiProps, hasAccounts, hasSudo),
[apiProps, hasAccounts, hasSudo]
);
const activeRoute = useMemo(
() => routeRef.current.find((route) => location.pathname.startsWith(`/${route.name}`)) || null,
[location]
);
const isLoading = !apiProps.isApiReady || !apiProps.isApiConnected;
return (
<div className={`${className} ui--highlight--border`}>
<div className='menuSection'>
<ChainInfo />
{activeRoute && (
<div className={`menuActive${isLoading ? ' isLoading' : ''} ui--highlight--border`}>
<Icon icon={activeRoute.icon} />
<span>{activeRoute.text}</span>
</div>
)}
<ul className='menuItems'>
{visibleGroups.map(({ name, routes }): React.ReactNode => (
<Grouping
key={name}
name={name}
routes={routes}
/>
))}
</ul>
</div>
<div className='menuSection'>
<ul className='menuItems'>
{externalRef.current.map((route): React.ReactNode => (
<Item
className='topLevel'
key={route.name}
route={route}
/>
))}
</ul>
</div>
<NodeInfo />
</div>
);
}
export default React.memo(styled(Menu)`
align-items: center;
background: #6a6662;
border-top: 0.5rem solid transparent;
display: flex;
justify-content: space-between;
padding: 0;
position: relative;
z-index: 220;
.menuSection {
align-items: flex-end;
align-self: flex-end;
display: flex;
}
.menuActive {
background: #fefcfa;
border-radius: 0.25rem 0.25rem 0 0;
padding: 1rem 1.5rem;
margin: 0 1.5rem;
&.isLoading {
background: #f5f3f1;
}
.ui--Icon {
margin-right: 0.5rem;
}
}
.menuItems {
background: #6a6662;
color: #f5f4f3;
flex: 1 1;
list-style: none;
margin: 0 2rem 0 0;
padding: 0;
> li {
display: inline-block;
}
}
`);

View File

@ -0,0 +1,24 @@
// Copyright 2017-2020 @polkadot/apps 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 { IconName } from '@fortawesome/fontawesome-svg-core';
import { Routes } from '@polkadot/apps-routing/types';
import React from 'react';
export interface ItemRoute {
Modal?: React.ComponentType<any>;
href?: string;
icon: IconName;
name: string;
text: string;
useCounter?: () => number | string | null;
}
export interface Group {
name: string;
routes: Routes;
}
export type Groups = Record<string, Group>;

View File

@ -1,135 +0,0 @@
// Copyright 2017-2020 @polkadot/apps 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 { ApiProps } from '@polkadot/react-api/types';
import { Route } from '@polkadot/apps-routing/types';
import { AccountId } from '@polkadot/types/interfaces';
import React, { useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom';
import { Badge, Icon, Menu, Tooltip } from '@polkadot/react-components';
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
import { findMissingApis } from '../endpoint';
const DUMMY_COUNTER = (): null => null;
interface Props {
isCollapsed: boolean;
onClick: () => void;
route: Route;
}
const disabledLog = new Map<string, string>();
const TOOLTIP_OFFSET = { right: -4 };
function logDisabled (route: string, message: string): void {
if (!disabledLog.get(route)) {
disabledLog.set(route, message);
console.warn(`Disabling ${route}: ${message}`);
}
}
function checkVisible (name: string, { api, isApiConnected, isApiReady }: ApiProps, hasAccounts: boolean, hasSudo: boolean, { isHidden, needsAccounts, needsApi, needsSudo }: Route['display']): boolean {
if (isHidden) {
return false;
} else if (needsAccounts && !hasAccounts) {
return false;
} else if (!needsApi) {
return true;
} else if (!isApiReady || !isApiConnected) {
return false;
} else if (needsSudo && !hasSudo) {
logDisabled(name, 'Sudo key not available');
return false;
}
const notFound = findMissingApis(api, needsApi);
if (notFound.length !== 0) {
logDisabled(name, `API not available: ${notFound.toString()}`);
}
return notFound.length === 0;
}
function Item ({ isCollapsed, onClick, route }: Props): React.ReactElement<Props> | null {
const { allAccounts, hasAccounts } = useAccounts();
const apiProps = useApi();
const sudoKey = useCall<AccountId>(apiProps.isApiReady && apiProps.api.query.sudo?.key);
const [hasSudo, setHasSudo] = useState(false);
const [isVisible, setIsVisible] = useState(false);
const count = (route.useCounter || DUMMY_COUNTER)();
useEffect((): void => {
setHasSudo(!!sudoKey && allAccounts.some((address) => sudoKey.eq(address)));
}, [allAccounts, sudoKey]);
useEffect((): void => {
const isVisible = checkVisible(route.name, apiProps, hasAccounts, hasSudo, route.display);
route.isIgnored = !isVisible;
setIsVisible(isVisible);
}, [apiProps, hasAccounts, hasSudo, route]);
if (!isVisible) {
return null;
}
const { Modal, icon, name, text } = route;
const body = (
<>
<Icon icon={icon} />
<span className='text'>{text}</span>
{!!count && (
<Badge
color='counter'
info={count}
/>
)}
<Tooltip
offset={TOOLTIP_OFFSET}
place='right'
text={text}
trigger={`nav-${name}`}
/>
</>
);
return (
<Menu.Item className='apps--SideBar-Item'>
{Modal
? (
<a
className='apps--SideBar-Item-NavLink'
data-for={`nav-${name}`}
data-tip
data-tip-disable={!isCollapsed}
onClick={onClick}
>
{body}
</a>
)
: (
<NavLink
activeClassName='apps--SideBar-Item-NavLink-active ui--highlight--border'
className='apps--SideBar-Item-NavLink'
data-for={`nav-${name}`}
data-tip
data-tip-disable={!isCollapsed}
onClick={onClick}
to={`/${name}`}
>
{body}
</NavLink>
)
}
</Menu.Item>
);
}
export default React.memo(Item);

View File

@ -1,294 +0,0 @@
// Copyright 2017-2020 @polkadot/apps 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 { Routes } from '@polkadot/apps-routing/types';
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import createRoutes from '@polkadot/apps-routing';
import { Button, ChainImg, Icon, Menu, media } from '@polkadot/react-components';
import { useIpfs } from '@polkadot/react-hooks';
import Endpoints from '../Endpoints';
import { SIDEBAR_MENU_THRESHOLD } from '../constants';
import { useTranslation } from '../translate';
import ChainInfo from './ChainInfo';
import Item from './Item';
import NodeInfo from './NodeInfo';
interface Props {
className?: string;
collapse: () => void;
handleResize: () => void;
isCollapsed: boolean;
isMenuOpen: boolean;
toggleMenu: () => void;
}
const SIDE_BORDER_WIDTH = '0.65rem';
const MENU_WIDTH_COLLAPSED = '4.2rem';
const MENU_WIDTH_EXPANDED = '12rem';
function AppsSidebar ({ className = '', collapse, handleResize, isCollapsed, isMenuOpen, toggleMenu }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { ipnsChain } = useIpfs();
const [modals, setModals] = useState<Record<string, boolean>>(
createRoutes(t).reduce((result: Record<string, boolean>, route): Record<string, boolean> => {
if (route && route.Modal) {
result[route.name] = false;
}
return result;
}, { network: false })
);
const routing = useMemo<Routes>(
() => createRoutes(t),
[t]
);
const _toggleModal = useCallback(
(name: string): () => void =>
(): void => setModals((modals: Record<string, boolean>) => ({
...modals,
[name]: !modals[name]
})),
[]
);
return (
<div className={`apps--SideBar-Wrapper ${className} ${isCollapsed ? 'collapsed' : 'expanded'}`}>
<ChainImg
className={`toggleImg ${isMenuOpen ? 'closed' : 'open delayed'}`}
onClick={toggleMenu}
/>
{modals.network && (
<Endpoints
offset={isCollapsed ? MENU_WIDTH_COLLAPSED : MENU_WIDTH_EXPANDED}
onClose={_toggleModal('network')}
/>
)}
{routing.map((route): React.ReactNode => (
route?.Modal
? route.Modal && modals[route.name]
? (
<route.Modal
key={route.name}
onClose={_toggleModal(route.name)}
/>
)
: <div key={route.name} />
: null
))}
<div className='apps--SideBar'>
<Menu
secondary
vertical
>
<div className='apps--SideBar-Scroll'>
<ChainInfo
isToggled={modals.network}
onClick={
ipnsChain
? undefined
: _toggleModal('network')
}
/>
{routing.map((route, index): React.ReactNode => (
route
? (
<Item
isCollapsed={isCollapsed}
key={route.name}
onClick={
route.Modal
? _toggleModal(route.name)
: handleResize
}
route={route}
/>
)
: (
<Menu.Divider
hidden
key={index}
/>
)
))}
<Menu.Divider hidden />
<Menu.Item className='apps--SideBar-Item'>
<a
className='apps--SideBar-Item-NavLink'
href='https://github.com/polkadot-js/apps'
rel='noopener noreferrer'
target='_blank'
>
<Icon icon='code-branch' /><span className='text'>{t<string>('nav.github', 'GitHub', { ns: 'apps-routing' })}</span>
</a>
</Menu.Item>
<Menu.Item className='apps--SideBar-Item'>
<a
className='apps--SideBar-Item-NavLink'
href='https://wiki.polkadot.network'
rel='noopener noreferrer'
target='_blank'
>
<Icon icon='book' /><span className='text'>{t<string>('nav.wiki', 'Wiki', { ns: 'apps-routing' })}</span>
</a>
</Menu.Item>
<Menu.Divider hidden />
{!isCollapsed && <NodeInfo />}
</div>
<div className={`apps--SideBar-collapse ${isCollapsed ? 'collapsed' : 'expanded'}`}>
<Button
icon={isCollapsed ? 'angle-double-right' : 'angle-double-left'}
isBasic
isCircular
onClick={collapse}
/>
</div>
</Menu>
<div
className='apps--SideBar-toggle'
onClick={collapse}
/>
</div>
</div>
);
}
export default React.memo(styled(AppsSidebar)`
display: flex;
position: relative;
z-index: 300;
&.collapsed {
width: ${MENU_WIDTH_COLLAPSED};
}
&.expanded {
width: ${MENU_WIDTH_EXPANDED};
}
.apps--SideBar {
align-items: center;
background: #4f5255;
box-sizing: border-box;
display: flex;
flex-flow: column;
height: auto;
position: relative;
transition: left 0.3s linear;
width: 100%;
.apps--SideBar-border {
border-top: ${SIDE_BORDER_WIDTH} solid transparent;
position: absolute;
left: 0;
right: 0;
top: 0;
}
.ui.vertical.menu {
display: flex;
height: 100vh;
margin: 0;
top: 0;
width: 100%;
position: sticky;
}
.apps--SideBar-Scroll {
align-items: center;
display: flex;
flex-direction: column;
height: 100vh;
overflow-y: auto;
width: 100%;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
width: 0px;
}
}
.apps--SideBar-Item {
align-self: flex-end;
flex-grow: 0;
padding: 0 !important;
position: relative;
width: inherit;
.text {
padding-left: 0.5rem;
}
.ui--Badge {
margin: 0;
position: absolute;
right: 0.5rem;
top: 0.55rem;
z-index: 1;
}
}
.apps--SideBar-collapse {
background: #4f5255;
bottom: 0;
left: 0;
padding: 0.75rem 0 .75rem 0.65rem;
position: sticky;
right: 0;
text-align: left;
width: 100%;
}
.apps--SideBar-toggle {
height: 100%;
position: absolute;
right: 0px;
top: 0px;
transition: all 0.2s;
width: 6px;
&:hover {
background: rgba(255,255,255,0.15);
cursor: pointer;
}
}
}
.toggleImg {
cursor: pointer;
height: 2.75rem;
left: 0.9rem;
opacity: 0;
position: absolute;
top: 0px;
transition: opacity 0.2s ease-in, top 0.2s ease-in;
width: 2.75rem;
&.delayed {
transition-delay: 0.4s;
}
&.open {
opacity: 1;
top: 0.9rem;
}
${media.DESKTOP`
opacity: 0 !important;
top: -2.9rem !important;
`}
}
@media only screen and (max-width: ${SIDEBAR_MENU_THRESHOLD}px) {
.apps--SideBar-collapse,
.apps--Sidebar-toggle {
display: none;
}
}
`);

View File

@ -1,15 +0,0 @@
// Copyright 2017-2020 @polkadot/apps 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 { ScreenSizes } from '@polkadot/react-components/constants';
export enum SideBarTransition {
COLLAPSED = 'COLLAPSED',
EXPANDED = 'EXPANDED',
EXPANDED_AND_MAXIMISED = 'EXPANDED_AND_MAXIMISED',
MINIMISED_AND_EXPANDED = 'MINIMISED_AND_EXPANDED'
}
export const SIDEBAR_MENU_THRESHOLD = ScreenSizes.DESKTOP;
export const SIDEBAR_TRANSITION_DURATION = 300;

View File

@ -51,7 +51,6 @@ export default React.memo(styled(BaseOverlay)`
border-bottom-left-radius: 0.25rem;
border-bottom-width: 1px;
border-left-width: 0.25rem;
right: 0;
line-height: 1.5em;
padding: 0 1rem;
position: fixed;

View File

@ -44,8 +44,9 @@ function CouncilApp ({ basePath, className }: Props): React.ReactElement<Props>
text: t<string>('Council overview')
},
{
count: numMotions,
name: 'motions',
text: t<string>('Motions ({{count}})', { replace: { count: numMotions } })
text: t<string>('Motions')
}
], [numMotions, t]);

View File

@ -24,14 +24,17 @@ function General ({ className = '' }: Props): React.ReactElement<Props> {
// tri-state: null = nothing changed, false = no reload, true = reload required
const [changed, setChanged] = useState<boolean | null>(null);
const [settings, setSettings] = useState(uiSettings.get());
const iconOptions = useMemo(
() => uiSettings.availableIcons.map((o): Option => createIdenticon(o, ['default'])),
[]
);
const prefixOptions = useMemo(
() => createSs58(t).map((o): Option | React.ReactNode => createOption(o, ['default'])),
[t]
);
const translateLanguages = useMemo(
() => createLanguages(t),
[t]
@ -54,10 +57,12 @@ function General ({ className = '' }: Props): React.ReactElement<Props> {
setSettings((settings) => ({ ...settings, [key]: value })),
[]
);
const _saveAndReload = useCallback(
() => saveAndReload(settings),
[settings]
);
const _save = useCallback(
(): void => {
save(settings);
@ -66,7 +71,7 @@ function General ({ className = '' }: Props): React.ReactElement<Props> {
[settings]
);
const { i18nLang, icon, ledgerConn, prefix, uiMode } = settings;
const { i18nLang, icon, ledgerConn, prefix } = settings;
return (
<div className={className}>
@ -88,15 +93,6 @@ function General ({ className = '' }: Props): React.ReactElement<Props> {
options={iconOptions}
/>
</div>
<div className='ui--row'>
<Dropdown
defaultValue={uiMode}
help={t<string>('Adjust the mode from basic (with a limited number of beginner-user-friendly apps) to full (with all basic & advanced apps available)')}
label={t<string>('interface operation mode')}
onChange={_handleChange('uiMode')}
options={uiSettings.availableUIModes}
/>
</div>
{isLedgerCapable() && (
<div className='ui--row'>
<Dropdown

View File

@ -28,12 +28,9 @@ function SettingsApp ({ basePath, onStatusChange }: Props): React.ReactElement<P
text: t<string>('General')
},
{
count: numExtensions,
name: 'metadata',
text: t<string>('Metadata {{count}}', {
replace: {
count: numExtensions ? `(${numExtensions})` : ''
}
})
text: t<string>('Metadata')
},
{
name: 'developer',

View File

@ -90,8 +90,9 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement<Pr
text: t<string>('Waiting')
},
{
count: slashes.reduce((count, [, unapplied]) => count + unapplied.length, 0),
name: 'slashes',
text: t<string>('Slashes ({{count}})', { replace: { count: slashes.reduce((count, [, unapplied]) => count + unapplied.length, 0) } })
text: t<string>('Slashes')
},
{
hasParams: true,

View File

@ -12,7 +12,7 @@ import Tooltip from './Tooltip';
interface Props {
className?: string;
color: 'counter' | 'green' | 'blue' | 'gray' | 'normal' | 'purple' | 'red' | 'transparent';
color: 'blue' | 'counter' | 'gray' | 'green' | 'highlight' | 'normal' | 'purple' | 'red' | 'transparent';
hover?: React.ReactNode;
icon?: IconName;
info?: React.ReactNode;
@ -20,6 +20,8 @@ interface Props {
onClick?: () => void;
}
const HIGHLIGHTS = ['counter', 'highlight'];
let badgeId = 0;
function Badge ({ className = '', color = 'normal', hover, icon, info, isSmall, onClick }: Props): React.ReactElement<Props> | null {
@ -27,11 +29,12 @@ function Badge ({ className = '', color = 'normal', hover, icon, info, isSmall,
const extraProps = hover
? { 'data-for': trigger, 'data-tip': true }
: {};
const isHighlight = HIGHLIGHTS.includes(color);
return (
<div
{...extraProps}
className={`ui--Badge${hover ? ' isTooltip' : ''}${isSmall ? ' isSmall' : ''}${onClick ? ' isClickable' : ''} ${color}Color ${className}`}
className={`ui--Badge${hover ? ' isTooltip' : ''}${isSmall ? ' isSmall' : ''}${onClick ? ' isClickable' : ''}${isHighlight ? ' ui--highlight--bg' : ''} ${color}Color ${className}`}
onClick={onClick}
>
{info || (icon && <Icon icon={icon} />)}
@ -47,7 +50,7 @@ function Badge ({ className = '', color = 'normal', hover, icon, info, isSmall,
export default React.memo(styled(Badge)`
border-radius: 16px;
color: #eee;
color: #eeedec;
display: inline-block;
font-size: 12px;
height: 22px;
@ -93,14 +96,13 @@ export default React.memo(styled(Badge)`
}
&.counterColor {
background: red;
margin: 0 0.5rem;
vertical-align: middle;
}
&.grayColor {
background: #eee !important;
color: #aaa;
background: #eeedec !important;
color: #aaa9a8;
}
&.redColor {

View File

@ -32,7 +32,7 @@ export default React.memo(styled(FilterOverlay)`
justify-content: flex-end;
position: absolute;
right: 5rem;
top: 0.4rem;
top: 0.525rem;
> div {
max-width: 35rem !important;

View File

@ -46,13 +46,13 @@ export default React.memo(styled(HelpOverlay)`
.help-button {
cursor: pointer;
font-size: 2rem;
padding: 1.25rem 1.5rem 0 0;
padding: 1rem 2.5rem 0 0;
}
> .help-button {
position: absolute;
right: 0rem;
top: 0rem;
top: 0.125rem;
}
.help-slideout {

View File

@ -139,7 +139,7 @@ export default React.memo(styled(Progress)`
div {
line-height: 1;
font-size: 1.25rem;
text-shadow: 0 0 2px #f5f4f3;
text-shadow: 0 0 2px #f5f3f1;
}
}

View File

@ -34,7 +34,7 @@ function Sidebar ({ button, children, className = '', onClose }: Props): React.R
}
export default React.memo(styled(Sidebar)(({ offset = 0, position }: Props) => `
background: #f5f4f3;
background: #f5f3f1;
bottom: 0;
box-shadow: ${position === 'right' ? '-6px' : '6px'} 0px 20px 0px rgba(0, 0, 0, 0.3);
margin-left: -0.125rem;

View File

@ -137,7 +137,7 @@ export default React.memo(styled(Body)`
tr {
&:nth-child(even) {
background: #faf9f8;
background: #faf8f6;
}
&:nth-child(odd) {

View File

@ -8,17 +8,19 @@ import React from 'react';
import { NavLink } from 'react-router-dom';
import styled from 'styled-components';
import Badge from '../Badge';
import Icon from '../Icon';
interface Props extends TabItem {
basePath: string;
className?: string;
count?: number;
index: number;
isSequence?: boolean;
num: number;
}
function Tab ({ basePath, className = '', hasParams, index, isExact, isRoot, isSequence, name, num, text }: Props): React.ReactElement<Props> {
function Tab ({ basePath, className = '', count, hasParams, index, isExact, isRoot, isSequence, name, num, text }: Props): React.ReactElement<Props> {
const to = isRoot
? basePath
: `${basePath}/${name}`;
@ -41,19 +43,24 @@ function Tab ({ basePath, className = '', hasParams, index, isExact, isRoot, isS
icon='arrow-right'
/>
)}
{!!count && (
<Badge
className='tabCounter'
color='counter'
info={count}
/>
)}
</NavLink>
);
}
export default React.memo(styled(Tab)`
border-bottom: 2px solid transparent;
color: rgba(0, 0, 0, 0.87) !important;
padding: 0.75rem 1.5rem;
color: #4e4e4e !important;
padding: 0.5rem 1.5rem 0.75rem;
&.tabLinkActive {
border-bottom-color: #e6e6e6;
color: rgba(0, 0, 0, 0.95);
font-weight: 700;
}
&:hover {
@ -64,6 +71,10 @@ export default React.memo(styled(Tab)`
}
}
.tabCounter {
margin: -1rem 0 -1rem 0.5rem;
}
.tabIcon {
margin-left: 0.75rem;
}

View File

@ -59,10 +59,13 @@ function Tabs ({ basePath, className = '', hidden, isSequence, items }: Props):
}
export default React.memo(styled(Tabs)`
align-items: flex-end;
background: #fefcfa;
border-bottom: 1px solid #e6e6e6;
margin: 0 -2em;
display: flex;
margin: 0 -3rem;
overflow-x: scroll;
padding: 2em 2em 0.75rem;
padding: 1.5rem 3rem 0;
text-align: left;
&::-webkit-scrollbar {

View File

@ -6,6 +6,7 @@ import React from 'react';
export interface TabItem {
alias?: string;
count?: number;
hasParams?: boolean;
isExact?: boolean;
isRoot?: boolean;

View File

@ -9,7 +9,7 @@ import Tooltip from './Tooltip';
interface Props {
className?: string;
color?: 'blue' | 'green' | 'grey' | 'orange' | 'pink' | 'red';
color?: 'blue' | 'green' | 'grey' | 'orange' | 'pink' | 'red' | 'yellow';
hover?: React.ReactNode;
isTag?: boolean;
label: React.ReactNode;
@ -82,6 +82,10 @@ export default React.memo(styled(Tag)`
background: #db2828;
}
&.yellowColor {
background: darkgoldenrod;
}
&.isTag {
border-radius: 0 0.25rem 0.25rem 0;
padding-left: 1.5em;

View File

@ -228,6 +228,7 @@ export default createGlobalStyle<Props>`
}
body {
background: #f5f3f1;
height: 100%;
margin: 0;
}
@ -301,8 +302,6 @@ export default createGlobalStyle<Props>`
}
main {
min-height: 100vh;
> section {
margin-bottom: 2em;
}

View File

@ -13,10 +13,11 @@ interface Props {
function NodeVersion ({ children, className = '', label }: Props): React.ReactElement<Props> {
const { systemVersion } = useApi();
const displayVersion = systemVersion.split('-').filter((_, index) => index <= 1).join('-');
return (
<div className={className}>
{label || ''}{systemVersion}{children}
{label || ''}{displayVersion}{children}
</div>
);
}