mirror of
https://github.com/pezkuwichain/pezkuwi-extension.git
synced 2026-11-09 01:37:05 +00:00
- @pezkuwi/dev, dev-test, dev-ts ^0.86.3 in the root and in every workspace package (they still asked for 0.84.3) - tsconfig.base target ES2022, the @pezkuwi/dev default; it carries the ErrorOptions type for `cause` - rethrown errors carry the original as `cause`; rejections are normalised to Error - page/messaging: the typed subscriber is called directly, not as `Function` - chrome mock: constant results through Promise.resolve - useLedger: drop a construction every branch below repeated - InputWithLabel: onKeyDown instead of the deprecated onKeyPress - Account: toggleActions + 1 instead of a pre-increment of a parameter - optional chains where a guard tested the same value - specs: mount<React.Component> so the wrappers are not `any`-typed - AccountsStore.set keeps its async override of KeyringStore.set: BaseStore.set handles its own failure, so it never rejects - extension-chains keeps its ChainProperties assertion: the package's own build does not load the type augmentation - the existing React Compiler findings (37, react-hooks rules only) are recorded in eslint-suppressions.json; a new one fails lint
309 lines
7.8 KiB
TypeScript
309 lines
7.8 KiB
TypeScript
// Copyright 2019-2026 @pezkuwi/extension-ui authors & contributors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import { faArrowLeft, faCog, faPlusCircle, faSearch } from '@fortawesome/free-solid-svg-icons';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
import logo from '../assets/pezkuwi-logo.png';
|
|
import { ActionContext } from '../components/index.js';
|
|
import InputFilter from '../components/InputFilter.js';
|
|
import Link from '../components/Link.js';
|
|
import { useOutsideClick, useTranslation } from '../hooks/index.js';
|
|
import { getConnectedTabsUrl } from '../messaging.js';
|
|
import { styled } from '../styled.js';
|
|
import MenuAdd from './MenuAdd.js';
|
|
import MenuSettings from './MenuSettings.js';
|
|
|
|
interface Props {
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
onFilter?: (filter: string) => void;
|
|
showAdd?: boolean;
|
|
showBackArrow?: boolean;
|
|
showConnectedAccounts?: boolean;
|
|
showSearch?: boolean;
|
|
showSettings?: boolean;
|
|
smallMargin?: boolean;
|
|
text?: React.ReactNode;
|
|
}
|
|
|
|
function Header ({ children, className = '', onFilter, showAdd, showBackArrow, showConnectedAccounts, showSearch, showSettings, smallMargin = false, text }: Props): React.ReactElement<Props> {
|
|
const [isAddOpen, setShowAdd] = useState(false);
|
|
const [isSettingsOpen, setShowSettings] = useState(false);
|
|
const [isSearchOpen, setShowSearch] = useState(false);
|
|
const [filter, setFilter] = useState('');
|
|
const [connectedTabsUrl, setConnectedTabsUrl] = useState<string[]>([]);
|
|
const { t } = useTranslation();
|
|
const addIconRef = useRef(null);
|
|
const addMenuRef = useRef<HTMLDivElement>(null);
|
|
const setIconRef = useRef(null);
|
|
const setMenuRef = useRef<HTMLDivElement>(null);
|
|
const isConnected = useMemo(() => connectedTabsUrl.length >= 1,
|
|
[connectedTabsUrl]);
|
|
const onAction = useContext(ActionContext);
|
|
|
|
useEffect(() => {
|
|
if (!showConnectedAccounts) {
|
|
return;
|
|
}
|
|
|
|
getConnectedTabsUrl()
|
|
.then((tabsUrl) => setConnectedTabsUrl(tabsUrl))
|
|
.catch(console.error);
|
|
}, [showConnectedAccounts]);
|
|
|
|
useOutsideClick([addIconRef, addMenuRef], (): void => {
|
|
isAddOpen && setShowAdd(!isAddOpen);
|
|
});
|
|
|
|
useOutsideClick([setIconRef, setMenuRef], (): void => {
|
|
isSettingsOpen && setShowSettings(!isSettingsOpen);
|
|
});
|
|
|
|
const _toggleAdd = useCallback(
|
|
() => setShowAdd((isAddOpen) => !isAddOpen),
|
|
[]
|
|
);
|
|
|
|
const _toggleSettings = useCallback(
|
|
() => setShowSettings((isSettingsOpen) => !isSettingsOpen),
|
|
[]
|
|
);
|
|
|
|
const _onChangeFilter = useCallback(
|
|
(filter: string) => {
|
|
setFilter(filter);
|
|
onFilter?.(filter);
|
|
},
|
|
[onFilter]
|
|
);
|
|
|
|
const _toggleSearch = useCallback(
|
|
(): void => {
|
|
if (isSearchOpen) {
|
|
_onChangeFilter('');
|
|
}
|
|
|
|
setShowSearch((isSearchOpen) => !isSearchOpen);
|
|
},
|
|
[_onChangeFilter, isSearchOpen]
|
|
);
|
|
|
|
const _onBackArrowClick = useCallback(
|
|
() => onAction('../index.js'),
|
|
[onAction]);
|
|
|
|
return (
|
|
<div className={`${className} ${smallMargin ? 'smallMargin' : ''}`}>
|
|
<div className='container'>
|
|
<div className='branding'>
|
|
{showBackArrow
|
|
? (
|
|
<FontAwesomeIcon
|
|
className='arrowLeftIcon'
|
|
icon={faArrowLeft}
|
|
onClick={_onBackArrowClick}
|
|
/>
|
|
)
|
|
: (
|
|
<img
|
|
className='logo'
|
|
src={logo}
|
|
/>
|
|
)
|
|
}
|
|
<span className='logoText'>{text || 'Pezkuwi'}</span>
|
|
</div>
|
|
{showSearch && (
|
|
<div className={`searchBarWrapper ${isSearchOpen ? 'selected' : ''}`}>
|
|
{showConnectedAccounts && !!isConnected && !isSearchOpen && (
|
|
<div className='connectedAccountsWrapper'>
|
|
<Link
|
|
className='connectedAccounts'
|
|
to={connectedTabsUrl.length === 1 ? `/url/manage/${encodeURIComponent(connectedTabsUrl[0])}` : '/auth-list'}
|
|
>
|
|
<span className='greenDot'>•</span>Connect Accounts
|
|
</Link>
|
|
</div>
|
|
)}
|
|
{isSearchOpen && (
|
|
<InputFilter
|
|
className='inputFilter'
|
|
onChange={_onChangeFilter}
|
|
placeholder={t('Search by name or network...')}
|
|
value={filter}
|
|
withReset
|
|
/>
|
|
)}
|
|
<FontAwesomeIcon
|
|
className={`searchIcon ${isSearchOpen ? 'selected' : ''}`}
|
|
icon={faSearch}
|
|
onClick={_toggleSearch}
|
|
size='lg'
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className='popupMenus'>
|
|
{showAdd && (
|
|
<div
|
|
className='popupToggle'
|
|
onClick={_toggleAdd}
|
|
ref={addIconRef}
|
|
>
|
|
<FontAwesomeIcon
|
|
className={`plusIcon ${isAddOpen ? 'selected' : ''}`}
|
|
icon={faPlusCircle}
|
|
size='lg'
|
|
/>
|
|
</div>
|
|
)}
|
|
{showSettings && (
|
|
<div
|
|
className='popupToggle'
|
|
data-toggle-settings
|
|
onClick={_toggleSettings}
|
|
ref={setIconRef}
|
|
>
|
|
<FontAwesomeIcon
|
|
className={`cogIcon ${isSettingsOpen ? 'selected' : ''}`}
|
|
icon={faCog}
|
|
size='lg'
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{isAddOpen && (
|
|
<MenuAdd reference={addMenuRef} />
|
|
)}
|
|
{isSettingsOpen && (
|
|
<MenuSettings reference={setMenuRef} />
|
|
)}
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default React.memo(styled(Header)<Props>`
|
|
max-width: 100%;
|
|
box-sizing: border-box;
|
|
font-weight: normal;
|
|
margin: 0;
|
|
position: relative;
|
|
margin-bottom: 25px;
|
|
|
|
&& {
|
|
padding: 0 0 0;
|
|
}
|
|
|
|
> .container {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
border-bottom: 1px solid var(--inputBorderColor);
|
|
min-height: 70px;
|
|
|
|
.branding {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
color: var(--labelColor);
|
|
font-family: var(--fontFamily);
|
|
text-align: center;
|
|
margin-left: 24px;
|
|
|
|
.logo {
|
|
height: 28px;
|
|
width: 28px;
|
|
margin: 8px 12px 12px 0;
|
|
}
|
|
|
|
.logoText {
|
|
color: var(--textColor);
|
|
font-family: var(--fontFamily);
|
|
font-size: 20px;
|
|
line-height: 27px;
|
|
}
|
|
}
|
|
|
|
.popupMenus, .searchBarWrapper {
|
|
align-self: center;
|
|
}
|
|
|
|
.connectedAccountsWrapper {
|
|
flex: 1;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.connectedAccounts {
|
|
border: 1px solid var(--inputBorderColor);
|
|
border-radius: 4px;
|
|
padding: 0 0.5rem;
|
|
|
|
.greenDot {
|
|
margin-right: 0.3rem;
|
|
font-size: 1.5rem;
|
|
color: var(--connectedDotColor);
|
|
padding-bottom: 0.2rem;
|
|
}
|
|
}
|
|
|
|
.searchBarWrapper {
|
|
flex: 1;
|
|
display: flex;
|
|
justify-content: end;
|
|
align-items: center;
|
|
|
|
.searchIcon {
|
|
margin-right: 8px;
|
|
|
|
&:hover {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
}
|
|
|
|
.popupToggle {
|
|
display: inline-block;
|
|
vertical-align: middle;
|
|
|
|
&:last-child {
|
|
margin-right: 24px;
|
|
}
|
|
|
|
&:hover {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
.inputFilter {
|
|
width: 100%
|
|
}
|
|
|
|
.popupToggle+.popupToggle {
|
|
margin-left: 8px;
|
|
}
|
|
}
|
|
|
|
.plusIcon, .cogIcon, .searchIcon {
|
|
color: var(--iconNeutralColor);
|
|
|
|
&.selected {
|
|
color: var(--primaryColor);
|
|
}
|
|
}
|
|
|
|
.arrowLeftIcon {
|
|
color: var(--labelColor);
|
|
margin-right: 1rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
&.smallMargin {
|
|
margin-bottom: 15px;
|
|
}
|
|
`);
|