Endpoint sidebar (#3282)

* Endpoint sidebar

* Collapsible

* Selection of new url

* Sidebar icon override

* url validity

* Remove  network selection from settings

* Updates with custom
This commit is contained in:
Jaco Greeff 2020-07-29 13:59:30 +02:00 committed by GitHub
parent 78d7006cd8
commit f7ecc45606
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 336 additions and 279 deletions

View File

@ -22,7 +22,7 @@ function createDev (t: TFunction): LinkOption[] {
dnslink: 'local',
info: 'local',
text: t<string>('rpc.local', 'Local Node (Own, 127.0.0.1:9944)', { ns: 'apps-config' }),
value: 'ws://127.0.0.1:9944/'
value: 'ws://127.0.0.1:9944'
}
];
}
@ -45,19 +45,20 @@ function createLive (t: TFunction): LinkOption[] {
dnslink: 'kusama',
info: 'kusama',
text: t<string>('rpc.kusama.parity', 'Kusama (Polkadot Canary, hosted by Parity)', { ns: 'apps-config' }),
value: 'wss://kusama-rpc.polkadot.io/'
value: 'wss://kusama-rpc.polkadot.io'
},
{
dnslink: 'kusama',
info: 'kusama',
text: t<string>('rpc.kusama.w3f', 'Kusama (Polkadot Canary, hosted by Web3 Foundation)', { ns: 'apps-config' }),
value: 'wss://cc3-5.kusama.network/'
value: 'wss://cc3-5.kusama.network'
},
{
dnslink: 'kusama',
info: 'kusama',
isDisabled: true,
text: t<string>('rpc.kusama.ava', 'Kusama (Polkadot Canary, user-run public nodes; see https://status.cloud.ava.do/)', { ns: 'apps-config' }),
value: 'wss://kusama.polkadot.cloud.ava.do/'
value: 'wss://kusama.polkadot.cloud.ava.do'
},
{
dnslink: 'centrifuge',
@ -101,12 +102,12 @@ function createTest (t: TFunction): LinkOption[] {
{
info: 'substrate',
text: t<string>('rpc.flamingfir', 'Flaming Fir (Substrate Testnet, hosted by Parity)', { ns: 'apps-config' }),
value: 'wss://substrate-rpc.parity.io/'
value: 'wss://substrate-rpc.parity.io'
},
{
info: 'nodle',
text: t<string>('rpc.arcadia', 'Arcadia (Nodle Testnet, hosted by Nodle)', { ns: 'apps-config' }),
value: 'wss://arcadia1.nodleprotocol.io/'
value: 'wss://arcadia1.nodleprotocol.io'
},
{
info: 'datahighway',

View File

@ -45,16 +45,16 @@ function Apps ({ className = '' }: Props): React.ReactElement<Props> {
isMenu: window.innerWidth < SIDEBAR_MENU_THRESHOLD
});
const uiHighlight = useMemo(
(): string | undefined => getSystemChainColor(systemChain, systemName),
() => getSystemChainColor(systemChain, systemName),
[systemChain, systemName]
);
const _collapse = useCallback(
(): void => setSidebar((sidebar: SidebarState) => saveSidebar({ ...sidebar, isCollapsed: !sidebar.isCollapsed })),
() => setSidebar((sidebar: SidebarState) => saveSidebar({ ...sidebar, isCollapsed: !sidebar.isCollapsed })),
[]
);
const _toggleMenu = useCallback(
(): void => setSidebar((sidebar: SidebarState) => saveSidebar({ ...sidebar, isCollapsed: false, isMenuOpen: true })),
() => setSidebar((sidebar: SidebarState) => saveSidebar({ ...sidebar, isCollapsed: false, isMenuOpen: true })),
[]
);
const _handleResize = useCallback(

View File

@ -14,10 +14,11 @@ import { useTranslation } from '../translate';
interface Props {
className?: string;
isToggled?: boolean;
onClick?: () => void;
}
function ChainInfo ({ className = '', onClick }: Props): React.ReactElement<Props> {
function ChainInfo ({ className = '', isToggled, onClick }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const runtimeVersion = useCall<RuntimeVersion>(api.rpc.state.subscribeRuntimeVersion, []);
@ -38,7 +39,7 @@ function ChainInfo ({ className = '', onClick }: Props): React.ReactElement<Prop
</div>
<Icon
className='dropdown'
icon='caret-down'
icon={isToggled ? 'caret-right' : 'caret-down'}
/>
</div>
</div>
@ -75,6 +76,7 @@ export default React.memo(styled(ChainInfo)`
.ui--Icon.dropdown {
flex: 0;
margin: 0;
width: 1rem;
}
> div.info {

View File

@ -0,0 +1,242 @@
// 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 { Option } from '@polkadot/apps-config/settings/types';
import React, { useCallback, useState } from 'react';
import styled from 'styled-components';
import { createEndpoints } from '@polkadot/apps-config/settings';
import { ChainImg, Icon, Input, Sidebar, Toggle } from '@polkadot/react-components';
import uiSettings from '@polkadot/ui-settings';
import { useTranslation } from '../translate';
interface Endpoint {
header: React.ReactNode;
networks: {
icon?: string;
name: string;
providers: {
name: string;
url: string;
}[]
}[];
}
interface Props {
className?: string;
offset?: number | string;
onClose: () => void;
}
interface UrlState {
apiUrl: string;
hasUrlChanged: boolean;
isUrlValid: boolean;
}
function textToParts (text: string): [string, string, string] {
const [first, remainder] = text.replace(')', '').split(' (');
const [middle, last] = remainder.split(', ');
return [first, middle, last];
}
function isValidUrl (url: string): boolean {
return (
// some random length... we probably want to parse via some lib
(url.length >= 7) &&
// check that it starts with a valid ws identifier
(url.startsWith('ws://') || url.startsWith('wss://'))
);
}
function combineEndpoints (endpoints: Option[]): Endpoint[] {
return endpoints.reduce((result: Endpoint[], e): Endpoint[] => {
if (e.isHeader) {
result.push({ header: e.text, networks: [] });
} else {
const [name, , providerName] = textToParts(e.text as string);
const prev = result[result.length - 1];
const prov = { name: providerName, url: e.value as string };
if (prev.networks[prev.networks.length - 1] && name === prev.networks[prev.networks.length - 1].name) {
prev.networks[prev.networks.length - 1].providers.push(prov);
} else {
prev.networks.push({
icon: e.info,
name,
providers: [prov]
});
}
}
return result;
}, []);
}
function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [endpoints] = useState(combineEndpoints(createEndpoints(t)));
const [{ apiUrl, hasUrlChanged, isUrlValid }, setApiUrl] = useState<UrlState>({ apiUrl: uiSettings.get().apiUrl, hasUrlChanged: false, isUrlValid: true });
const [openIndex, setOpenIndex] = useState('');
const _setApiUrl = useCallback(
(apiUrl: string) => () => setApiUrl({ apiUrl, hasUrlChanged: uiSettings.get().apiUrl !== apiUrl, isUrlValid: true }),
[]
);
const _setOpenIndex = useCallback(
(index: string) => () => setOpenIndex((openIndex) => openIndex === index ? '' : index),
[]
);
const _onChangeCustom = useCallback(
(apiUrl: string) => setApiUrl({ apiUrl, hasUrlChanged: uiSettings.get().apiUrl !== apiUrl, isUrlValid: isValidUrl(apiUrl) }),
[]
);
const _onClose = useCallback(
(): void => {
if (hasUrlChanged && isUrlValid) {
const settings = uiSettings.get();
uiSettings.set({ ...settings, apiUrl });
window.location.reload();
}
onClose();
},
[apiUrl, hasUrlChanged, isUrlValid, onClose]
);
return (
<Sidebar
className={className}
closeIcon={(hasUrlChanged && isUrlValid) ? 'sync' : 'times'}
offset={offset}
onClose={_onClose}
position='left'
>
{endpoints.map(({ header, networks }, typeIndex): React.ReactNode => (
<div
className='endpointType'
key={typeIndex}
>
<div className='endpointHeader'>{header}</div>
{networks.map(({ icon, name, providers }, netIndex): React.ReactNode => {
const isSelected = providers.some(({ url }) => url === apiUrl);
const index = `${typeIndex}:${netIndex}`;
const isOpen = openIndex === index;
return (
<div
className={`endpointGroup${isOpen ? ' isOpen' : ''}${isSelected ? ' isSelected' : ''}`}
key={index}
onClick={_setOpenIndex(index)}
>
<div className='endpointSection'>
<ChainImg
className='endpointIcon'
logo={icon === 'local' ? 'empty' : icon}
/>
<div className='endpointValue'>{name}</div>
{!isSelected && (
<Icon
className='endpointOpen'
icon={isOpen ? 'caret-up' : 'caret-down'}
/>
)}
</div>
{providers.map(({ name, url }): React.ReactNode => (
<Toggle
className='endpointProvider'
key={url}
label={name}
onChange={_setApiUrl(url)}
value={apiUrl === url}
/>
))}
</div>
);
})}
</div>
))}
<Input
className='endpointCustom'
isError={!isUrlValid}
isFull
label={t<string>('Custom url')}
onChange={_onChangeCustom}
value={apiUrl}
/>
</Sidebar>
);
}
export default React.memo(styled(Endpoints)`
.endpointCustom {
margin-top: 0.5rem;
}
.endpointType {
&+.endpointType {
margin-top: 1rem;
}
}
.endpointGroup {
border-radius: 0.25rem;
cursor: pointer;
padding: 0.375rem;
&.isOpen,
&.isSelected {
.endpointProvider {
display: block;
}
}
&:hover {
background: #fffefd;
}
}
.endpointHeader {
font-size: 0.78571429em;
font-weight: 700;
line-height: 1;
padding: 0.5rem 0 1rem;
text-transform: uppercase;
}
.endpointIcon {
height: 32px;
margin-right: 0.75rem;
width: 32px;
}
.endpointProvider {
display: none;
padding: 0.25rem;
text-align: right;
}
.endpointSection {
align-items: center;
display: flex;
justify-content: flex-start;
position: relative;
.endpointOpen {
position: absolute;
right: 0.5rem;
top: 0.5rem;
}
&+.endpointProvider {
margin-top: -0.5rem;
}
}
`);

View File

@ -65,7 +65,7 @@ function Item ({ isCollapsed, onClick, route }: Props): React.ReactElement<Props
const count = (route.useCounter || DUMMY_COUNTER)();
useEffect((): void => {
setHasSudo(!!sudoKey && allAccounts.some((address): boolean => sudoKey.eq(address)));
setHasSudo(!!sudoKey && allAccounts.some((address) => sudoKey.eq(address)));
}, [allAccounts, sudoKey]);
useEffect((): void => {

View File

@ -41,12 +41,9 @@ export default React.memo(styled(NodeInfo)`
> div {
margin-bottom: -0.125em;
> div {
display: inline-block;
}
&.spacer {
margin-bottom: 0.5rem;
}
}
`);

View File

@ -10,9 +10,9 @@ import createRoutes from '@polkadot/apps-routing';
import { Button, ChainImg, Icon, Menu, media } from '@polkadot/react-components';
import { SIDEBAR_MENU_THRESHOLD } from '../constants';
import NetworkModal from '../modals/Network';
import { useTranslation } from '../translate';
import ChainInfo from './ChainInfo';
import Endpoints from './Endpoints';
import Item from './Item';
import NodeInfo from './NodeInfo';
@ -25,7 +25,11 @@ interface Props {
toggleMenu: () => void;
}
function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuOpen, toggleMenu }: Props): React.ReactElement<Props> {
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 [modals, setModals] = useState<Record<string, boolean>>(
createRoutes(t).reduce((result: Record<string, boolean>, route): Record<string, boolean> => {
@ -57,6 +61,12 @@ function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuO
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]
@ -69,16 +79,16 @@ function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuO
: <div key={route.name} />
: null
))}
{modals.network && (
<NetworkModal onClose={_toggleModal('network')}/>
)}
<div className='apps--SideBar'>
<Menu
secondary
vertical
>
<div className='apps--SideBar-Scroll'>
<ChainInfo onClick={_toggleModal('network')} />
<ChainInfo
isToggled={modals.network}
onClick={_toggleModal('network')}
/>
{routing.map((route, index): React.ReactNode => (
route
? (
@ -142,19 +152,17 @@ function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuO
);
}
const sideBorderWidth = '0.65rem';
export default React.memo(styled(SideBar)`
export default React.memo(styled(AppsSidebar)`
display: flex;
position: relative;
z-index: 300;
&.collapsed {
width: 4.2rem;
width: ${MENU_WIDTH_COLLAPSED};
}
&.expanded {
width: 12rem;
width: ${MENU_WIDTH_EXPANDED};
}
.apps--SideBar {
@ -169,7 +177,7 @@ export default React.memo(styled(SideBar)`
width: 100%;
.apps--SideBar-border {
border-top: ${sideBorderWidth} solid transparent;
border-top: ${SIDE_BORDER_WIDTH} solid transparent;
position: absolute;
left: 0;
right: 0;

View File

@ -10,7 +10,6 @@ import '@polkadot/react-components/i18n';
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import { HashRouter } from 'react-router-dom';
import store from 'store';
import { ThemeProvider } from 'styled-components';
import { Api } from '@polkadot/react-api';
import Queue from '@polkadot/react-components/Status/Queue';
@ -28,13 +27,6 @@ if (!rootElement) {
throw new Error(`Unable to find element with id '${rootId}'`);
}
// cleanups for old/unused storage items
store.each((_, key): void => {
if (key.startsWith('hooks:sessionSlashes:')) {
store.remove(key);
}
});
ReactDOM.render(
<Suspense fallback='...'>
<ThemeProvider theme={theme}>

View File

@ -1,35 +0,0 @@
// Copyright 2017-2020 @polkadot/app-accounts 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 { Modal } from '@polkadot/react-components';
import General from '@polkadot/app-settings/General';
import { useTranslation } from '../translate';
interface Props {
className?: string;
onClose: () => void;
}
function NetworkModal ({ className = '', onClose }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<Modal
className={className}
header={t<string>('Select Network')}
size='large'
>
<Modal.Content>
<General
isModalContent
onClose={onClose}
/>
</Modal.Content>
</Modal>
);
}
export default React.memo(NetworkModal);

View File

@ -7,22 +7,19 @@ import { Option } from '@polkadot/apps-config/settings/types';
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { createLanguages, createSs58 } from '@polkadot/apps-config/settings';
import { isLedgerCapable } from '@polkadot/react-api';
import { Button, ButtonCancel, Dropdown, Modal } from '@polkadot/react-components';
import { Button, Dropdown } from '@polkadot/react-components';
import uiSettings, { SettingsStruct } from '@polkadot/ui-settings';
import { useTranslation } from './translate';
import { createIdenticon, createOption, save, saveAndReload } from './util';
import SelectUrl from './SelectUrl';
interface Props {
className?: string;
isModalContent?: boolean;
onClose: () => void;
}
const ledgerConnOptions = uiSettings.availableLedgerConn;
function General ({ className = '', isModalContent, onClose }: Props): React.ReactElement<Props> {
function General ({ className = '' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
// tri-state: null = nothing changed, false = no reload, true = reload required
const [changed, setChanged] = useState<boolean | null>(null);
@ -58,7 +55,7 @@ function General ({ className = '', isModalContent, onClose }: Props): React.Rea
[]
);
const _saveAndReload = useCallback(
(): void => saveAndReload(settings),
() => saveAndReload(settings),
[settings]
);
const _save = useCallback(
@ -70,75 +67,56 @@ function General ({ className = '', isModalContent, onClose }: Props): React.Rea
);
const { i18nLang, icon, ledgerConn, prefix, uiMode } = settings;
const networkSelector = <SelectUrl onChange={_handleChange('apiUrl')} />;
return (
<div className={className}>
{isModalContent
? (
<Modal.Columns>
<Modal.Column>{networkSelector}</Modal.Column>
<Modal.Column>
{t<string>('The RPC node can be selected from the pre-defined list or manually entered, depending on the chain you wish to connect to.')}
</Modal.Column>
</Modal.Columns>
)
: networkSelector
}
{!isModalContent && (
<>
<div className='ui--row'>
<Dropdown
defaultValue={prefix}
help={t<string>('Override the default ss58 prefix for address generation')}
label={t<string>('address prefix')}
onChange={_handleChange('prefix')}
options={prefixOptions}
/>
</div>
<div className='ui--row'>
<Dropdown
defaultValue={icon}
help={t<string>('Override the default identity icon display with a specific theme')}
label={t<string>('default icon theme')}
onChange={_handleChange('icon')}
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
defaultValue={ledgerConn}
help={t<string>('Manage your connection to Ledger S')}
label={t<string>('manage hardware connections')}
onChange={_handleChange('ledgerConn')}
options={ledgerConnOptions}
/>
</div>
)}
<div className='ui--row'>
<Dropdown
defaultValue={i18nLang}
label={t<string>('default interface language')}
onChange={_handleChange('i18nLang')}
options={translateLanguages}
/>
</div>
</>
<div className='ui--row'>
<Dropdown
defaultValue={prefix}
help={t<string>('Override the default ss58 prefix for address generation')}
label={t<string>('address prefix')}
onChange={_handleChange('prefix')}
options={prefixOptions}
/>
</div>
<div className='ui--row'>
<Dropdown
defaultValue={icon}
help={t<string>('Override the default identity icon display with a specific theme')}
label={t<string>('default icon theme')}
onChange={_handleChange('icon')}
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
defaultValue={ledgerConn}
help={t<string>('Manage your connection to Ledger S')}
label={t<string>('manage hardware connections')}
onChange={_handleChange('ledgerConn')}
options={ledgerConnOptions}
/>
</div>
)}
<div className='ui--row'>
<Dropdown
defaultValue={i18nLang}
label={t<string>('default interface language')}
onChange={_handleChange('i18nLang')}
options={translateLanguages}
/>
</div>
<Button.Group>
{isModalContent && (
<ButtonCancel onClick={onClose} />
)}
<Button
icon='save'
isDisabled={changed === null}

View File

@ -1,134 +0,0 @@
// Copyright 2017-2020 @polkadot/app-settings 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 { TFunction } from 'i18next';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { createEndpoints } from '@polkadot/apps-config/settings';
import { Dropdown, Input, Toggle } from '@polkadot/react-components';
import uiSettings from '@polkadot/ui-settings';
import { useTranslation } from './translate';
import { createOption } from './util';
interface Props {
className?: string;
onChange: (url: string) => void;
}
interface StateUrl {
isValid: boolean;
url: string;
}
interface State extends StateUrl {
isCustom: boolean;
}
// check the validity of the url
function isValidUrl (url: string): boolean {
return (
// some random length... we probably want to parse via some lib
(url.length >= 7) &&
// check that it starts with a valid ws identifier
(url.startsWith('ws://') || url.startsWith('wss://'))
);
}
// sanitize a url and check it for validity
function makeUrl (_url: string): StateUrl {
const url = _url.trim();
const isValid = isValidUrl(url);
return { isValid, url };
}
// this allows us to retrieve the initial state by reading the settings and the applying
// validation on-top of the values retrieved
function getInitialState (t: TFunction): State {
const url = uiSettings.get().apiUrl;
return {
isCustom: createEndpoints(t).reduce((isCustom: boolean, { value }): boolean => {
return isCustom && value !== url;
}, true),
isValid: isValidUrl(url),
url
};
}
function SelectUrl ({ className = '', onChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [info, setInfo] = useState(getInitialState(t));
const { isCustom, isValid, url } = info;
const translatedEndpoints = useMemo(
() => createEndpoints(t).map((option) => createOption(option, ['local'])),
[t]
);
useEffect((): void => {
onChange && info.isValid && onChange(info.url);
// the issue here is that the onChange callback changes each and every render... so Houston, we have
// a desperate issue here :(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [info]);
const _onChangeUrl = useCallback(
(url: string) => setInfo((info: State) => ({ ...info, ...makeUrl(url) })),
[]
);
const _onChangeCustom = useCallback(
(isCustom: boolean) => setInfo({
...makeUrl(
isCustom
? info.url
: (createEndpoints(t).find(({ value }) => !!value) || { value: 'ws://127.0.0.1:9944' }).value as string
),
isCustom
}),
[info, t]
);
const help = t<string>('Select the remote endpoint, either from the dropdown on manual entered via the custom toggle');
const label = t<string>('remote node/endpoint to connect to');
return (
<div className={className}>
<div className='ui--row'>{
isCustom
? <Input
defaultValue={url}
help={help}
isError={!isValid}
label={label}
onChange={_onChangeUrl}
/>
: <Dropdown
defaultValue={url}
help={help}
label={label}
onChange={_onChangeUrl}
options={translatedEndpoints}
/>
}</div>
<Toggle
className='settings--customToggle'
label={t<string>('custom endpoint')}
onChange={_onChangeCustom}
value={isCustom}
/>
</div>
);
}
export default React.memo(styled(SelectUrl)`
position: relative;
.settings--customToggle {
position: absolute;
bottom: 1.375rem;
right: 3.5rem;
}
`);

View File

@ -71,7 +71,7 @@ export function save (settings: SettingsStruct): void {
export function saveAndReload (settings: SettingsStruct): void {
save(settings);
// HACK This is terribe, but since the API needs to re-connect, but since
// HACK This is terrible, but since the API needs to re-connect, but since
// the API does not yet handle re-connections properly, it is what it is
window.location.reload();
}

View File

@ -2,6 +2,8 @@
// 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 React from 'react';
import styled from 'styled-components';
@ -10,17 +12,18 @@ import Button from './Button';
interface Props {
children: React.ReactNode;
className?: string;
offset?: number;
closeIcon?: IconName;
offset?: number | string;
onClose: () => void;
position: 'left' | 'right';
}
function Sidebar ({ children, className = '', onClose }: Props): React.ReactElement<Props> {
function Sidebar ({ children, className = '', closeIcon = 'times', onClose }: Props): React.ReactElement<Props> {
return (
<div className={`ui--Sidebar ${className}`}>
<Button
className='ui--Sidebar-close'
icon='times'
icon={closeIcon}
isBasic
isCircular
onClick={onClose}
@ -34,9 +37,12 @@ export default React.memo(styled(Sidebar)(({ offset = 0, position }: Props) => `
background: #f5f4f3;
bottom: 0;
box-shadow: ${position === 'right' ? '-6px' : '6px'} 0px 20px 0px rgba(0, 0, 0, 0.3);
margin-left: -0.125rem;
max-width: 24rem;
min-width: 24rem;
position: fixed;
padding: 1rem;
overflow-y: auto;
top: 0;
z-index: 999;
${position}: ${offset};