summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2025-01-13 15:03:31 +0100
committerMarkus Pettersson <markus.pettersson@mullvad.net>2025-01-13 15:03:31 +0100
commit650204703b7d820db9f3ad76ebd2c84d0cfc3f21 (patch)
treeb67d591d5fbccf35c00c4f3c25f7be03231c2a1b
parent525aaa00a22f0a7c4ba85791334466518493b4cb (diff)
parent9d1f8c5406442327078473c9e272d4c0c3701175 (diff)
downloadmullvadvpn-650204703b7d820db9f3ad76ebd2c84d0cfc3f21.tar.xz
mullvadvpn-650204703b7d820db9f3ad76ebd2c84d0cfc3f21.zip
Merge branch 'change-paddings-in-main-nav-header-des-1150'
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx18
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx59
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx16
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx16
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx14
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx9
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx9
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ErrorView.tsx8
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx19
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorView.tsx16
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorViewStyles.tsx5
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Filter.tsx27
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/HeaderBar.tsx253
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/InfoButton.tsx40
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Layout.tsx7
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx11
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/MultihopSettings.tsx18
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/NavigationBar.tsx230
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/NavigationBarStyles.tsx57
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/NavigationContainer.tsx62
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/NavigationScrollbars.tsx73
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/OpenVpnSettings.tsx32
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ProblemReport.tsx18
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SelectLanguage.tsx26
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Settings.tsx19
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SettingsImport.tsx60
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SettingsTextImport.tsx34
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Shadowsocks.tsx26
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx9
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Support.tsx26
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx11
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/UdpOverTcp.tsx26
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/UserInterfaceSettings.tsx26
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx62
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/WireguardSettings.tsx32
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/AppMainHeader.tsx80
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderAccountButton.tsx32
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderDeviceInfo.tsx54
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderSettingsButton.tsx26
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/index.ts3
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/AppNavigationHeader.tsx30
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/AppNavigationHeaderBackButton.tsx32
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/AppNavigationHeaderInfoButton.tsx5
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/index.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/cell/Selector.tsx9
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsGroup.tsx2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/main-view/MainView.tsx7
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx45
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/select-location/SpecialLocationList.tsx4
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/AppInfoView.tsx16
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/changelog/ChangelogView.tsx19
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/atoms/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/atoms/logo/Logo.tsx41
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/index.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/molecules/Button.tsx12
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/molecules/IconButton.tsx13
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/index.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/MainHeader.tsx55
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/components/MainHeaderIconButton.tsx5
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/components/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/NavigationHeader.tsx53
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/NavigationHeaderContext.tsx24
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderButtonGroup.tsx9
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderIconButton.tsx5
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderTitle.tsx22
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/index.ts3
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/typography/Text.tsx25
73 files changed, 978 insertions, 1042 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx
index 1cc666910e..115196a49d 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx
@@ -7,6 +7,7 @@ import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
import { useEffectEvent } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import AccountNumberLabel from './AccountNumberLabel';
import {
AccountContainer,
@@ -23,7 +24,6 @@ import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGrou
import DeviceInfoButton from './DeviceInfoButton';
import { BackAction } from './KeyboardNavigation';
import { Footer, Layout, SettingsContainer } from './Layout';
-import { NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar';
import { RedeemVoucherButton } from './RedeemVoucher';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
@@ -49,16 +49,12 @@ export default function Account() {
<BackAction action={history.pop}>
<Layout>
<SettingsContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('account-view', 'Account')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('account-view', 'Account')
+ }
+ />
<AccountContainer>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx
index bf1a2025df..83142d29d7 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx
@@ -14,6 +14,7 @@ import { generateRoutePath } from '../lib/routeHelpers';
import { RoutePath } from '../lib/routes';
import { useBoolean } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import * as Cell from './cell';
import {
ContextMenu,
@@ -26,13 +27,7 @@ import InfoButton from './InfoButton';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer, SettingsContent, SettingsNavigationScrollbars } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationInfoButton,
- NavigationItems,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
import { SmallButton, SmallButtonColor } from './SmallButton';
@@ -42,7 +37,7 @@ const StyledContextMenuButton = styled(Cell.Icon)({
marginRight: Spacings.spacing3,
});
-const StyledMethodInfoButton = styled(InfoButton)({
+const StyledMethodInfoButton = styled(InfoButton).attrs({ size: 'small' })({
marginRight: Spacings.spacing4,
});
@@ -82,32 +77,28 @@ export default function ApiAccessMethods() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('navigation-bar', 'API access')
- }
- </TitleBarItem>
- <NavigationInfoButton
- message={[
- messages.pgettext(
- 'api-access-methods-view',
- 'The app needs to communicate with a Mullvad API server to log you in, fetch server lists, and other critical operations.',
- ),
- messages.pgettext(
- 'api-access-methods-view',
- 'On some networks, where various types of censorship are being used, the API servers might not be directly reachable.',
- ),
- messages.pgettext(
- 'api-access-methods-view',
- 'This feature allows you to circumvent that censorship by adding custom ways to access the API via proxies and similar methods.',
- ),
- ]}
- />
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('navigation-bar', 'API access')
+ }>
+ <AppNavigationHeader.InfoButton
+ message={[
+ messages.pgettext(
+ 'api-access-methods-view',
+ 'The app needs to communicate with a Mullvad API server to log you in, fetch server lists, and other critical operations.',
+ ),
+ messages.pgettext(
+ 'api-access-methods-view',
+ 'On some networks, where various types of censorship are being used, the API servers might not be directly reachable.',
+ ),
+ messages.pgettext(
+ 'api-access-methods-view',
+ 'This feature allows you to circumvent that censorship by adding custom ways to access the API via proxies and similar methods.',
+ ),
+ ]}
+ />
+ </AppNavigationHeader>
<SettingsNavigationScrollbars fillContainer>
<SettingsContent>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx
index 264b389f84..a3ebac27be 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx
@@ -10,19 +10,15 @@ import { Spacings } from '../lib/foundations';
import { useHistory } from '../lib/history';
import { useBoolean } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
import InfoButton from './InfoButton';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
import PageSlider from './PageSlider';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
import { SmallButton, SmallButtonColor } from './SmallButton';
@@ -44,11 +40,7 @@ export default function DaitaSettings() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>{strings.daita}</TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader title={strings.daita} />
<NavigationScrollbars>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx
index fee86b00cc..6e3fe6540c 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx
@@ -4,17 +4,13 @@ import styled from 'styled-components';
import { Spacings } from '../lib/foundations';
import { useHistory } from '../lib/history';
import { useBoolean } from '../lib/utility-hooks';
+import { AppNavigationHeader } from './';
import * as AppButton from './AppButton';
import { measurements } from './common-styles';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
const StyledContent = styled.div({
@@ -36,11 +32,7 @@ export default function Debug() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>Developer tools</TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader title="Developer tools" />
<NavigationScrollbars>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx
index c591c816f4..cf1c413f6b 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx
@@ -4,17 +4,12 @@ import { colors } from '../../config.json';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import { useSelector } from '../redux/store';
+import { AppMainHeader } from './app-main-header';
import * as AppButton from './AppButton';
import { bigText, measurements, smallText } from './common-styles';
import CustomScrollbars from './CustomScrollbars';
-import { calculateHeaderBarStyle, DefaultHeaderBar } from './HeaderBar';
import ImageView from './ImageView';
-import { Container, Footer } from './Layout';
-import { Layout } from './Layout';
-
-export const StyledHeader = styled(DefaultHeaderBar)({
- flex: 0,
-});
+import { Container, Footer, Layout } from './Layout';
export const StyledCustomScrollbars = styled(CustomScrollbars)({
flex: 1,
@@ -59,7 +54,10 @@ export function DeviceRevokedView() {
return (
<Layout>
- <StyledHeader barStyle={calculateHeaderBarStyle(tunnelState)} />
+ <AppMainHeader variant="basedOnConnectionStatus" size="basedOnLoginStatus">
+ <AppMainHeader.AccountButton />
+ <AppMainHeader.SettingsButton />
+ </AppMainHeader>
<StyledCustomScrollbars fillContainer>
<StyledContainer>
<StyledBody>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx
index 79c7de6861..6dec1e8eb6 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx
@@ -14,11 +14,12 @@ import { useApiAccessMethodTest } from '../lib/api-access-methods';
import { useHistory } from '../lib/history';
import { useLastDefinedValue } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import { SettingsForm } from './cell/SettingsForm';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer, SettingsContent, SettingsNavigationScrollbars } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
-import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
import { NamedProxyForm } from './ProxyForm';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
import { SmallButton } from './SmallButton';
@@ -95,11 +96,7 @@ function AccessMethodForm() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>{title}</TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader title={title} />
<SettingsNavigationScrollbars fillContainer>
<SettingsContent>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx
index 2ab8f81083..6d45eda229 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx
@@ -6,11 +6,12 @@ import { useBridgeSettingsUpdater } from '../lib/constraint-updater';
import { useHistory } from '../lib/history';
import { useBoolean } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import { SettingsForm } from './cell/SettingsForm';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer, SettingsContent, SettingsNavigationScrollbars } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
-import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
import { ProxyForm } from './ProxyForm';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
import { SmallButton, SmallButtonColor } from './SmallButton';
@@ -66,11 +67,7 @@ function CustomBridgeForm() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>{title}</TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader title={title} />
<SettingsNavigationScrollbars fillContainer>
<SettingsContent>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ErrorView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ErrorView.tsx
index 08ed7b0962..647dc78b4a 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ErrorView.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ErrorView.tsx
@@ -2,10 +2,10 @@ import React from 'react';
import styled from 'styled-components';
import { colors } from '../../config.json';
+import { AppMainHeader } from './app-main-header';
import { measurements } from './common-styles';
-import { HeaderBarSettingsButton } from './HeaderBar';
import ImageView from './ImageView';
-import { Container, Header, Layout } from './Layout';
+import { Container, Layout } from './Layout';
const StyledContainer = styled(Container)({
flex: 1,
@@ -56,7 +56,9 @@ interface ErrorViewProps {
export default function ErrorView(props: ErrorViewProps) {
return (
<Layout>
- <Header>{!props.settingsUnavailable && <HeaderBarSettingsButton />}</Header>
+ <AppMainHeader logoVariant="none">
+ {!props.settingsUnavailable && <AppMainHeader.SettingsButton />}
+ </AppMainHeader>
<StyledContainer>
<StyledContent>
<Logo height={106} width={106} source="logo-icon" />
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx
index 2433a29644..df154e628b 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx
@@ -14,11 +14,11 @@ import { generateRoutePath } from '../lib/routeHelpers';
import { RoutePath } from '../lib/routes';
import account from '../redux/account/actions';
import { useSelector } from '../redux/store';
+import { AppMainHeader } from './app-main-header';
import * as AppButton from './AppButton';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import { hugeText, measurements, tinyText } from './common-styles';
import CustomScrollbars from './CustomScrollbars';
-import { calculateHeaderBarStyle, DefaultHeaderBar, HeaderBarStyle } from './HeaderBar';
import ImageView from './ImageView';
import { Container, Footer, Layout } from './Layout';
import {
@@ -28,10 +28,6 @@ import {
RedeemVoucherSubmitButton,
} from './RedeemVoucher';
-export const StyledHeader = styled(DefaultHeaderBar)({
- flex: 0,
-});
-
export const StyledCustomScrollbars = styled(CustomScrollbars)({
flex: 1,
});
@@ -261,12 +257,15 @@ function HeaderBar() {
const isNewAccount = useSelector(
(state) => state.account.status.type === 'ok' && state.account.status.method === 'new_account',
);
- const tunnelState = useSelector((state) => state.connection.status);
- const headerBarStyle = isNewAccount
- ? HeaderBarStyle.default
- : calculateHeaderBarStyle(tunnelState);
- return <StyledHeader barStyle={headerBarStyle} />;
+ return (
+ <AppMainHeader
+ variant={isNewAccount ? 'default' : 'basedOnConnectionStatus'}
+ size="basedOnLoginStatus">
+ <AppMainHeader.AccountButton />
+ <AppMainHeader.SettingsButton />
+ </AppMainHeader>
+ );
}
function useFinishedCallback() {
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorView.tsx
index a98d3e441c..744632c6ed 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorView.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorView.tsx
@@ -9,6 +9,7 @@ import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { useSelector } from '../redux/store';
+import { AppMainHeader } from './app-main-header';
import * as AppButton from './AppButton';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import * as Cell from './cell';
@@ -21,13 +22,11 @@ import {
StyledContainer,
StyledCustomScrollbars,
StyledDeviceLabel,
- StyledHeader,
StyledMessage,
StyledModalCellContainer,
StyledStatusIcon,
StyledTitle,
} from './ExpiredAccountErrorViewStyles';
-import { calculateHeaderBarStyle, HeaderBarStyle } from './HeaderBar';
import ImageView from './ImageView';
import { Footer, Layout } from './Layout';
import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
@@ -50,15 +49,9 @@ function ExpiredAccountErrorViewComponent() {
const { push } = useHistory();
const { disconnectTunnel } = useAppContext();
- const connection = useSelector((state) => state.connection);
-
const { recoveryAction } = useRecoveryAction();
const isNewAccount = useIsNewAccount();
- const headerBarStyle = isNewAccount
- ? HeaderBarStyle.default
- : calculateHeaderBarStyle(connection.status);
-
const onDisconnect = useCallback(async () => {
try {
await disconnectTunnel();
@@ -74,7 +67,12 @@ function ExpiredAccountErrorViewComponent() {
return (
<Layout>
- <StyledHeader barStyle={headerBarStyle} />
+ <AppMainHeader
+ variant={isNewAccount ? 'default' : 'basedOnConnectionStatus'}
+ size="basedOnLoginStatus">
+ <AppMainHeader.AccountButton />
+ <AppMainHeader.SettingsButton />
+ </AppMainHeader>
<StyledCustomScrollbars fillContainer>
<StyledContainer>
<StyledBody>{isNewAccount ? <WelcomeView /> : <Content />}</StyledBody>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorViewStyles.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorViewStyles.tsx
index f41178a627..5e62751570 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorViewStyles.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorViewStyles.tsx
@@ -5,13 +5,8 @@ import AccountNumberLabel from './AccountNumberLabel';
import * as Cell from './cell';
import { hugeText, measurements, tinyText } from './common-styles';
import CustomScrollbars from './CustomScrollbars';
-import { DefaultHeaderBar } from './HeaderBar';
import { Container } from './Layout';
-export const StyledHeader = styled(DefaultHeaderBar)({
- flex: 0,
-});
-
export const StyledAccountNumberLabel = styled(AccountNumberLabel)({
fontFamily: 'Open Sans',
lineHeight: '20px',
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Filter.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Filter.tsx
index 940563fd87..790215cab4 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Filter.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Filter.tsx
@@ -15,6 +15,7 @@ import { useNormalRelaySettings, useTunnelProtocol } from '../lib/relay-settings
import { useBoolean } from '../lib/utility-hooks';
import { IRelayLocationCountryRedux } from '../redux/settings/reducers';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import Accordion from './Accordion';
import * as AppButton from './AppButton';
import { AriaInputGroup, AriaLabel } from './AriaGroup';
@@ -24,13 +25,8 @@ import { normalText } from './common-styles';
import ImageView from './ImageView';
import { BackAction } from './KeyboardNavigation';
import { Footer, Layout, SettingsContainer } from './Layout';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
const StyledNavigationScrollbars = styled(NavigationScrollbars)({
backgroundColor: colors.darkBlue,
@@ -83,16 +79,13 @@ export default function Filter() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar alwaysDisplayBarTitle={true}>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('filter-nav', 'Filter')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('filter-nav', 'Filter')
+ }
+ titleVisible
+ />
<StyledNavigationScrollbars>
<FilterByOwnership
ownership={ownership}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/HeaderBar.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/HeaderBar.tsx
deleted file mode 100644
index d0f2bde6ef..0000000000
--- a/desktop/packages/mullvad-vpn/src/renderer/components/HeaderBar.tsx
+++ /dev/null
@@ -1,253 +0,0 @@
-import React, { useCallback } from 'react';
-import { sprintf } from 'sprintf-js';
-import styled from 'styled-components';
-
-import { colors } from '../../config.json';
-import { closeToExpiry, formatRemainingTime, hasExpired } from '../../shared/account-expiry';
-import { TunnelState } from '../../shared/daemon-rpc-types';
-import { messages } from '../../shared/gettext';
-import { capitalizeEveryWord } from '../../shared/string-helpers';
-import { transitions, useHistory } from '../lib/history';
-import { RoutePath } from '../lib/routes';
-import { useSelector } from '../redux/store';
-import { tinyText } from './common-styles';
-import { FocusFallback } from './Focus';
-import ImageView from './ImageView';
-
-export enum HeaderBarStyle {
- default = 'default',
- defaultDark = 'defaultDark',
- error = 'error',
- success = 'success',
-}
-
-const headerBarStyleColorMap = {
- [HeaderBarStyle.default]: colors.blue,
- [HeaderBarStyle.defaultDark]: colors.darkBlue,
- [HeaderBarStyle.error]: colors.red,
- [HeaderBarStyle.success]: colors.green,
-};
-
-interface IHeaderBarContainerProps {
- $barStyle?: HeaderBarStyle;
- $accountInfoVisible: boolean;
- $unpinnedWindow: boolean;
-}
-
-const HeaderBarContainer = styled.header<IHeaderBarContainerProps>((props) => ({
- padding: '15px 11px 0px 16px',
- minHeight: props.$accountInfoVisible ? '80px' : '68px',
- height: props.$accountInfoVisible ? '80px' : '68px',
- backgroundColor: headerBarStyleColorMap[props.$barStyle ?? HeaderBarStyle.default],
- transitionProperty: 'height, min-height',
- transitionDuration: '250ms',
- transitionTimingFunction: 'ease-in-out',
-}));
-
-const HeaderBarContent = styled.div({
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'flex-end',
- height: '38px',
-});
-
-interface IHeaderBarProps {
- barStyle?: HeaderBarStyle;
- className?: string;
- children?: React.ReactNode;
- showAccountInfo?: boolean;
-}
-
-export default function HeaderBar(props: IHeaderBarProps) {
- const unpinnedWindow = useSelector((state) => state.settings.guiSettings.unpinnedWindow);
-
- return (
- <HeaderBarContainer
- $barStyle={props.barStyle}
- className={props.className}
- $accountInfoVisible={props.showAccountInfo ?? false}
- $unpinnedWindow={unpinnedWindow}>
- <HeaderBarContent>{props.children}</HeaderBarContent>
- {props.showAccountInfo && <HeaderBarDeviceInfo />}
- </HeaderBarContainer>
- );
-}
-
-const BrandContainer = styled.div({
- display: 'flex',
- flex: 1,
- alignItems: 'center',
-});
-
-const Title = styled(ImageView)({
- opacity: 0.8,
- marginLeft: '9px',
-});
-
-export function Brand(props: React.HTMLAttributes<HTMLDivElement>) {
- return (
- <BrandContainer {...props}>
- <ImageView width={38} height={38} source="logo-icon" />
- <Title height={15.4} source="logo-text" />
- </BrandContainer>
- );
-}
-
-const StyledAccountInfo = styled.div({
- display: 'flex',
- marginTop: '2px',
- maxWidth: '100%',
-});
-
-const StyledDeviceLabel = styled.div(tinyText, {
- fontSize: '10px',
- color: colors.white80,
- whiteSpace: 'nowrap',
- overflow: 'hidden',
- textOverflow: 'ellipsis',
-});
-
-const StyledTimeLeftLabel = styled.div(tinyText, {
- fontSize: '10px',
- color: colors.white80,
- marginLeft: '16px',
- whiteSpace: 'nowrap',
-});
-
-function HeaderBarDeviceInfo() {
- const deviceName = useSelector((state) => state.account.deviceName);
- const accountExpiry = useSelector((state) => state.account.expiry);
- const isOutOfTime = accountExpiry ? hasExpired(accountExpiry) : false;
- const formattedExpiry = isOutOfTime
- ? sprintf(messages.ngettext('1 day', '%d days', 0), 0)
- : accountExpiry
- ? formatRemainingTime(accountExpiry)
- : '';
-
- return (
- <StyledAccountInfo>
- <StyledDeviceLabel>
- {sprintf(
- // TRANSLATORS: A label that will display the newly created device name to inform the user
- // TRANSLATORS: about it.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(deviceName)s - The name of the current device
- messages.pgettext('device-management', 'Device name: %(deviceName)s'),
- {
- deviceName: capitalizeEveryWord(deviceName ?? ''),
- },
- )}
- </StyledDeviceLabel>
- {accountExpiry && !closeToExpiry(accountExpiry) && !isOutOfTime && (
- <StyledTimeLeftLabel>
- {sprintf(messages.pgettext('device-management', 'Time left: %(timeLeft)s'), {
- timeLeft: formattedExpiry,
- })}
- </StyledTimeLeftLabel>
- )}
- </StyledAccountInfo>
- );
-}
-
-const HeaderBarSettingsButtonContainer = styled.button({
- cursor: 'default',
- padding: '5px',
- marginLeft: '3px',
- backgroundColor: 'transparent',
- border: 'none',
-});
-
-const HeaderBarAccountButtonContainer = styled(HeaderBarSettingsButtonContainer)({
- marginRight: '11px',
-});
-
-const StyledHeaderBarImageView = styled(ImageView)((props) => ({
- [`${HeaderBarSettingsButtonContainer}:hover &&`]: {
- backgroundColor: props.tintHoverColor,
- },
-}));
-
-interface IHeaderBarSettingsButtonProps {
- disabled?: boolean;
-}
-
-export function HeaderBarSettingsButton(props: IHeaderBarSettingsButtonProps) {
- const history = useHistory();
-
- const openSettings = useCallback(() => {
- if (!props.disabled) {
- history.push(RoutePath.settings, { transition: transitions.show });
- }
- }, [history, props.disabled]);
-
- return (
- <HeaderBarSettingsButtonContainer
- onClick={openSettings}
- aria-label={messages.gettext('Settings')}>
- <StyledHeaderBarImageView
- height={24}
- width={24}
- source="icon-settings"
- tintColor={props.disabled ? colors.white40 : colors.white60}
- tintHoverColor={props.disabled ? colors.white40 : colors.white80}
- />
- </HeaderBarSettingsButtonContainer>
- );
-}
-
-export function HeaderBarAccountButton() {
- const history = useHistory();
- const openAccount = useCallback(
- () => history.push(RoutePath.account, { transition: transitions.show }),
- [history],
- );
-
- return (
- <HeaderBarAccountButtonContainer
- onClick={openAccount}
- data-testid="account-button"
- aria-label={messages.gettext('Account settings')}>
- <StyledHeaderBarImageView
- height={24}
- width={24}
- source="icon-account"
- tintColor={colors.white60}
- tintHoverColor={colors.white80}
- />
- </HeaderBarAccountButtonContainer>
- );
-}
-
-export function DefaultHeaderBar(props: IHeaderBarProps) {
- const loggedIn = useSelector((state) => state.account.status.type === 'ok');
-
- return (
- <HeaderBar showAccountInfo={loggedIn} {...props}>
- <FocusFallback>
- <Brand />
- </FocusFallback>
- {loggedIn && <HeaderBarAccountButton />}
- <HeaderBarSettingsButton />
- </HeaderBar>
- );
-}
-
-export function calculateHeaderBarStyle(tunnelState: TunnelState): HeaderBarStyle {
- switch (tunnelState.state) {
- case 'disconnected':
- return HeaderBarStyle.error;
- case 'connecting':
- case 'connected':
- return HeaderBarStyle.success;
- case 'error':
- return !tunnelState.details.blockingError ? HeaderBarStyle.success : HeaderBarStyle.error;
- case 'disconnecting':
- switch (tunnelState.details) {
- case 'block':
- case 'reconnect':
- return HeaderBarStyle.success;
- case 'nothing':
- return HeaderBarStyle.error;
- }
- }
-}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/InfoButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/InfoButton.tsx
index 7f77f115e7..8243e5b3be 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/InfoButton.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/InfoButton.tsx
@@ -1,20 +1,10 @@
-import styled from 'styled-components';
-
import { colors } from '../../config.json';
import { messages } from '../../shared/gettext';
+import { Button, IconButton, IconButtonProps } from '../lib/components';
import { useBoolean } from '../lib/utility-hooks';
-import * as AppButton from './AppButton';
import ImageView from './ImageView';
import { ModalAlert, ModalAlertType } from './Modal';
-const StyledInfoButton = styled.button({
- margin: '0 16px 0 8px',
- borderWidth: 0,
- padding: 0,
- cursor: 'default',
- backgroundColor: 'transparent',
-});
-
interface IInfoIconProps {
className?: string;
size?: number;
@@ -34,39 +24,35 @@ export function InfoIcon(props: IInfoIconProps) {
);
}
-export interface IInfoButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
+export interface InfoButtonProps extends Omit<IconButtonProps, 'icon'> {
+ title?: string;
message?: string | Array<string>;
children?: React.ReactNode;
- title?: string;
- size?: number;
- tintColor?: string;
- tintHoverColor?: string;
}
-export default function InfoButton(props: IInfoButtonProps) {
- const { message, children, size, tintColor, tintHoverColor, ...otherProps } = props;
+export default function InfoButton({ title, message, children, ...props }: InfoButtonProps) {
const [isOpen, show, hide] = useBoolean(false);
return (
<>
- <StyledInfoButton
+ <IconButton
+ icon="icon-info"
onClick={show}
aria-label={messages.pgettext('accessibility', 'More information')}
- {...otherProps}>
- <InfoIcon size={size} tintColor={tintColor} tintHoverColor={tintHoverColor} />
- </StyledInfoButton>
+ {...props}
+ />
<ModalAlert
isOpen={isOpen}
- title={props.title}
- message={props.message}
+ title={title}
+ message={message}
type={ModalAlertType.info}
buttons={[
- <AppButton.BlueButton key="back" onClick={hide}>
+ <Button key="back" onClick={hide}>
{messages.gettext('Got it!')}
- </AppButton.BlueButton>,
+ </Button>,
]}
close={hide}>
- {props.children}
+ {children}
</ModalAlert>
</>
);
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Layout.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Layout.tsx
index 62babaac38..ff7c8c7943 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Layout.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Layout.tsx
@@ -3,12 +3,7 @@ import styled from 'styled-components';
import { Flex } from '../lib/components';
import { Colors, Spacings } from '../lib/foundations';
import { measurements } from './common-styles';
-import HeaderBar from './HeaderBar';
-import { NavigationScrollbars } from './NavigationBar';
-
-export const Header = styled(HeaderBar)({
- flex: 0,
-});
+import { NavigationScrollbars } from './NavigationScrollbars';
export const Container = styled.div({
display: 'flex',
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx
index 0d234bd66b..08e11b7628 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx
@@ -12,11 +12,11 @@ import accountActions from '../redux/account/actions';
import { LoginState } from '../redux/account/reducers';
import { useSelector } from '../redux/store';
import Accordion from './Accordion';
+import { AppMainHeader } from './app-main-header';
import * as AppButton from './AppButton';
import { AriaControlGroup, AriaControlled, AriaControls } from './AriaGroup';
-import { Brand, HeaderBarSettingsButton } from './HeaderBar';
import ImageView from './ImageView';
-import { Container, Header, Layout } from './Layout';
+import { Container, Layout } from './Layout';
import {
StyledAccountDropdownContainer,
StyledAccountDropdownItem,
@@ -126,10 +126,9 @@ class Login extends React.Component<IProps, IState> {
return (
<Layout>
- <Header>
- <Brand />
- <HeaderBarSettingsButton disabled={!allowInteraction} />
- </Header>
+ <AppMainHeader>
+ <AppMainHeader.SettingsButton disabled={!allowInteraction} />
+ </AppMainHeader>
<Container>
<StyledTopInfo>
{this.props.showBlockMessage ? <BlockMessage /> : this.getStatusIcon()}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/MultihopSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/MultihopSettings.tsx
index e7e16b395a..d885dd8533 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/MultihopSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/MultihopSettings.tsx
@@ -8,18 +8,14 @@ import { Flex } from '../lib/components';
import { useRelaySettingsUpdater } from '../lib/constraint-updater';
import { useHistory } from '../lib/history';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
import { StyledIllustration } from './DaitaSettings';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
export default function MultihopSettings() {
@@ -30,13 +26,7 @@ export default function MultihopSettings() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {messages.pgettext('wireguard-settings-view', 'Multihop')}
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader title={messages.pgettext('wireguard-settings-view', 'Multihop')} />
<NavigationScrollbars>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NavigationBar.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NavigationBar.tsx
deleted file mode 100644
index 680393a6c7..0000000000
--- a/desktop/packages/mullvad-vpn/src/renderer/components/NavigationBar.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-import React, { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
-import styled from 'styled-components';
-
-import { colors } from '../../config.json';
-import { messages } from '../../shared/gettext';
-import { useAppContext } from '../context';
-import { transitions, useHistory } from '../lib/history';
-import { useCombinedRefs, useEffectEvent } from '../lib/utility-hooks';
-import CustomScrollbars, { CustomScrollbarsRef, IScrollEvent } from './CustomScrollbars';
-import InfoButton from './InfoButton';
-import { BackActionContext } from './KeyboardNavigation';
-import {
- StyledBackBarItemButton,
- StyledBackBarItemIcon,
- StyledNavigationBar,
- StyledNavigationBarSeparator,
- StyledNavigationItems,
- StyledTitleBarItemLabel,
-} from './NavigationBarStyles';
-
-interface INavigationContainerProps {
- children?: React.ReactNode;
-}
-
-interface INavigationContainerState {
- showsBarTitle: boolean;
- showsBarSeparator: boolean;
-}
-
-const NavigationScrollContext = React.createContext({
- showsBarTitle: false,
- showsBarSeparator: false,
- onScroll(_event: IScrollEvent): void {
- throw Error('NavigationScrollContext provider missing');
- },
-});
-
-export class NavigationContainer extends React.Component<
- INavigationContainerProps,
- INavigationContainerState
-> {
- public state = {
- showsBarTitle: false,
- showsBarSeparator: false,
- };
-
- public componentDidMount() {
- this.updateBarAppearance({ scrollLeft: 0, scrollTop: 0 });
- }
-
- public render() {
- return (
- <NavigationScrollContext.Provider
- value={{
- ...this.state,
- onScroll: this.onScroll,
- }}>
- {this.props.children}
- </NavigationScrollContext.Provider>
- );
- }
-
- public onScroll = (event: IScrollEvent) => {
- this.updateBarAppearance(event);
- };
-
- private updateBarAppearance(event: IScrollEvent) {
- // that's where SettingsHeader.HeaderTitle intersects the navigation bar
- const showsBarSeparator = event.scrollTop > 11;
-
- // that's when SettingsHeader.HeaderTitle goes behind the navigation bar
- const showsBarTitle = event.scrollTop > 20;
-
- if (
- this.state.showsBarSeparator !== showsBarSeparator ||
- this.state.showsBarTitle !== showsBarTitle
- ) {
- this.setState({ showsBarSeparator, showsBarTitle });
- }
- }
-}
-
-interface INavigationScrollbarsProps {
- className?: string;
- fillContainer?: boolean;
- children?: React.ReactNode;
-}
-
-export const NavigationScrollbars = React.forwardRef(function NavigationScrollbarsT(
- props: INavigationScrollbarsProps,
- forwardedRef?: React.Ref<CustomScrollbarsRef>,
-) {
- const history = useHistory();
- const { setNavigationHistory } = useAppContext();
- const { onScroll } = useContext(NavigationScrollContext);
-
- const ref = useRef<CustomScrollbarsRef>();
- const combinedRefs = useCombinedRefs(forwardedRef, ref);
-
- const beforeunload = useEffectEvent(() => {
- if (ref.current) {
- history.recordScrollPosition(ref.current.getScrollPosition());
- setNavigationHistory(history.asObject);
- }
- });
-
- useEffect(() => {
- window.addEventListener('beforeunload', beforeunload);
- return () => window.removeEventListener('beforeunload', beforeunload);
- }, []);
-
- const onMount = useEffectEvent(() => {
- const location = history.location;
- if (history.action === 'POP') {
- ref.current?.scrollTo(...location.state.scrollPosition);
- }
- });
-
- const onUnmount = useEffectEvent(() => {
- if (history.action === 'PUSH' && ref.current) {
- history.recordScrollPosition(ref.current.getScrollPosition());
- setNavigationHistory(history.asObject);
- }
- });
-
- useLayoutEffect(() => {
- onMount();
- return () => onUnmount();
- }, []);
-
- const handleScroll = useCallback(
- (event: IScrollEvent) => {
- onScroll(event);
- },
- [onScroll],
- );
-
- return (
- <CustomScrollbars
- ref={combinedRefs}
- className={props.className}
- fillContainer={props.fillContainer}
- onScroll={handleScroll}>
- {props.children}
- </CustomScrollbars>
- );
-});
-
-const TitleBarItemContext = React.createContext({
- visible: false,
-});
-
-interface INavigationBarProps {
- children?: React.ReactNode;
- alwaysDisplayBarTitle?: boolean;
-}
-
-export const NavigationBar = function NavigationBarT(props: INavigationBarProps) {
- const { showsBarSeparator, showsBarTitle } = useContext(NavigationScrollContext);
-
- return (
- <StyledNavigationBar>
- <TitleBarItemContext.Provider
- value={{ visible: props.alwaysDisplayBarTitle || showsBarTitle }}>
- {props.children}
- </TitleBarItemContext.Provider>
- {showsBarSeparator && <StyledNavigationBarSeparator />}
- </StyledNavigationBar>
- );
-};
-
-interface INavigationItemsProps {
- children: React.ReactNode;
-}
-
-export function NavigationItems(props: INavigationItemsProps) {
- const { parentBackAction } = useContext(BackActionContext);
- return (
- <StyledNavigationItems>
- {parentBackAction && <BackBarItem />}
- {props.children}
- </StyledNavigationItems>
- );
-}
-
-interface ITitleBarItemProps {
- children?: React.ReactText;
-}
-
-export const TitleBarItem = React.memo(function TitleBarItemT(props: ITitleBarItemProps) {
- const { visible } = useContext(TitleBarItemContext);
- return <StyledTitleBarItemLabel $visible={visible}>{props.children}</StyledTitleBarItemLabel>;
-});
-
-export function BackBarItem() {
- const history = useHistory();
- // Compare the transition name with dismiss to infer wheter or not the view will slide
- // horizontally or vertically and then use matching button.
- const backIcon = useMemo(
- () => history.getPopTransition().name !== transitions.dismiss.name,
- [history],
- );
- const { parentBackAction } = useContext(BackActionContext);
- const iconSource = backIcon ? 'icon-back' : 'icon-close-down';
- const ariaLabel = backIcon ? messages.gettext('Back') : messages.gettext('Close');
-
- return (
- <StyledBackBarItemButton aria-label={ariaLabel} onClick={parentBackAction}>
- <StyledBackBarItemIcon source={iconSource} tintColor={colors.white40} width={24} />
- </StyledBackBarItemButton>
- );
-}
-
-const navigationRightHandSideButton: React.CSSProperties = {
- justifySelf: 'end',
- borderWidth: 0,
- padding: 0,
- margin: 0,
- cursor: 'default',
- backgroundColor: 'transparent',
-};
-
-export const NavigationBarButton = styled.button({ ...navigationRightHandSideButton });
-export const NavigationInfoButton = styled(InfoButton).attrs({
- size: 24,
- tintColor: colors.white40,
- tintHoverColor: colors.white60,
-})({
- ...navigationRightHandSideButton,
-});
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NavigationBarStyles.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NavigationBarStyles.tsx
deleted file mode 100644
index eb4472c900..0000000000
--- a/desktop/packages/mullvad-vpn/src/renderer/components/NavigationBarStyles.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import styled from 'styled-components';
-
-import { colors } from '../../config.json';
-import { normalText } from './common-styles';
-import ImageView from './ImageView';
-
-export const StyledNavigationBarSeparator = styled.div({
- backgroundColor: 'rgba(0, 0, 0, 0.2)',
- position: 'absolute',
- bottom: 0,
- left: 0,
- right: 0,
- height: '1px',
-});
-
-export const StyledNavigationItems = styled.div({
- flex: 1,
- display: 'grid',
- gridTemplateColumns: '1fr auto 1fr',
- alignItems: 'center',
-});
-
-export const StyledNavigationBar = styled.nav({
- flex: 0,
- padding: '12px',
-});
-
-export const StyledTitleBarItemLabel = styled.h1<{ $visible?: boolean }>(normalText, (props) => ({
- fontWeight: 400,
- lineHeight: '22px',
- color: colors.white,
- padding: '0 5px',
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- opacity: props.$visible ? 1 : 0,
- transition: 'opacity 250ms ease-in-out',
-}));
-
-export const StyledBackBarItemButton = styled.button({
- justifySelf: 'start',
- borderWidth: 0,
- padding: 0,
- margin: 0,
- cursor: 'default',
- display: 'flex',
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: 'transparent',
-});
-
-export const StyledBackBarItemIcon = styled(ImageView)({
- marginRight: '8px',
- [StyledBackBarItemButton + ':hover &&']: {
- backgroundColor: colors.white60,
- },
-});
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NavigationContainer.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NavigationContainer.tsx
new file mode 100644
index 0000000000..946a66c05f
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/NavigationContainer.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+
+import { IScrollEvent } from './CustomScrollbars';
+
+interface NavigationContainerProps {
+ children?: React.ReactNode;
+}
+
+interface NavigationContainerState {
+ showsBarTitle: boolean;
+}
+
+export const NavigationScrollContext = React.createContext<{
+ showsBarTitle: boolean;
+ onScroll: (event: IScrollEvent) => void;
+}>({
+ showsBarTitle: false,
+ onScroll: () => {
+ throw new Error('NavigationScrollContext provider is missing.');
+ },
+});
+
+export class NavigationContainer extends React.Component<
+ NavigationContainerProps,
+ NavigationContainerState
+> {
+ state: NavigationContainerState = {
+ showsBarTitle: false,
+ };
+
+ componentDidMount() {
+ this.updateBarAppearance({ scrollLeft: 0, scrollTop: 0 });
+ }
+
+ render() {
+ const { children } = this.props;
+ const { showsBarTitle } = this.state;
+
+ return (
+ <NavigationScrollContext.Provider
+ value={{
+ showsBarTitle,
+ onScroll: this.onScroll,
+ }}>
+ {children}
+ </NavigationScrollContext.Provider>
+ );
+ }
+
+ private onScroll = (event: IScrollEvent) => {
+ this.updateBarAppearance(event);
+ };
+
+ private updateBarAppearance = ({ scrollTop }: IScrollEvent) => {
+ // Show the bar title when user scrolls past page title
+ const showsBarTitle = scrollTop > 20;
+
+ if (this.state.showsBarTitle !== showsBarTitle) {
+ this.setState({ showsBarTitle });
+ }
+ };
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NavigationScrollbars.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NavigationScrollbars.tsx
new file mode 100644
index 0000000000..81583b1c4e
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/NavigationScrollbars.tsx
@@ -0,0 +1,73 @@
+import React, { useCallback, useContext, useEffect, useLayoutEffect, useRef } from 'react';
+
+import { useAppContext } from '../context';
+import { useHistory } from '../lib/history';
+import { useCombinedRefs, useEffectEvent } from '../lib/utility-hooks';
+import CustomScrollbars, { CustomScrollbarsRef, IScrollEvent } from './CustomScrollbars';
+import { NavigationScrollContext } from './NavigationContainer';
+
+export interface NavigationScrollbarsProps {
+ className?: string;
+ fillContainer?: boolean;
+ children?: React.ReactNode;
+}
+
+export const NavigationScrollbars = React.forwardRef(function NavigationScrollbarsT(
+ props: NavigationScrollbarsProps,
+ forwardedRef?: React.Ref<CustomScrollbarsRef>,
+) {
+ const history = useHistory();
+ const { setNavigationHistory } = useAppContext();
+ const { onScroll } = useContext(NavigationScrollContext);
+
+ const ref = useRef<CustomScrollbarsRef>();
+ const combinedRefs = useCombinedRefs(forwardedRef, ref);
+
+ const beforeunload = useEffectEvent(() => {
+ if (ref.current) {
+ history.recordScrollPosition(ref.current.getScrollPosition());
+ setNavigationHistory(history.asObject);
+ }
+ });
+
+ useEffect(() => {
+ window.addEventListener('beforeunload', beforeunload);
+ return () => window.removeEventListener('beforeunload', beforeunload);
+ }, []);
+
+ const onMount = useEffectEvent(() => {
+ const location = history.location;
+ if (history.action === 'POP') {
+ ref.current?.scrollTo(...location.state.scrollPosition);
+ }
+ });
+
+ const onUnmount = useEffectEvent(() => {
+ if (history.action === 'PUSH' && ref.current) {
+ history.recordScrollPosition(ref.current.getScrollPosition());
+ setNavigationHistory(history.asObject);
+ }
+ });
+
+ useLayoutEffect(() => {
+ onMount();
+ return () => onUnmount();
+ }, []);
+
+ const handleScroll = useCallback(
+ (event: IScrollEvent) => {
+ onScroll(event);
+ },
+ [onScroll],
+ );
+
+ return (
+ <CustomScrollbars
+ ref={combinedRefs}
+ className={props.className}
+ fillContainer={props.fillContainer}
+ onScroll={handleScroll}>
+ {props.children}
+ </CustomScrollbars>
+ );
+});
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/OpenVpnSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/OpenVpnSettings.tsx
index 37e72b13e7..d1288b30a1 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/OpenVpnSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/OpenVpnSettings.tsx
@@ -17,19 +17,15 @@ import { useRelaySettingsUpdater } from '../lib/constraint-updater';
import { useHistory } from '../lib/history';
import { formatHtml } from '../lib/html-formatter';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
import Selector, { SelectorItem } from './cell/Selector';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
import { ModalMessage } from './Modal';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
const MIN_MSSFIX_VALUE = 1000;
@@ -70,19 +66,15 @@ export default function OpenVpnSettings() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {sprintf(
- // TRANSLATORS: Title label in navigation bar
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(openvpn)s - Will be replaced with "OpenVPN"
- messages.pgettext('openvpn-settings-nav', '%(openvpn)s settings'),
- { openvpn: strings.openvpn },
- )}
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={sprintf(
+ // TRANSLATORS: Title label in navigation bar
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(openvpn)s - Will be replaced with "OpenVPN"
+ messages.pgettext('openvpn-settings-nav', '%(openvpn)s settings'),
+ { openvpn: strings.openvpn },
+ )}
+ />
<NavigationScrollbars>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ProblemReport.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ProblemReport.tsx
index 36ddc09c45..e4213d871f 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ProblemReport.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ProblemReport.tsx
@@ -20,13 +20,13 @@ import { useHistory } from '../lib/history';
import { useEffectEvent } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
import support from '../redux/support/actions';
+import { AppNavigationHeader } from './';
import * as AppButton from './AppButton';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import ImageView from './ImageView';
import { BackAction } from './KeyboardNavigation';
import { Footer, Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
-import { NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar';
import {
StyledContent,
StyledContentContainer,
@@ -66,16 +66,12 @@ function ProblemReportComponent() {
<BackAction action={history.pop}>
<Layout>
<SettingsContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('support-view', 'Report a problem')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('support-view', 'Report a problem')
+ }
+ />
<StyledContentContainer>
<Header />
<Content />
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SelectLanguage.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SelectLanguage.tsx
index 84f572fc5a..91ee78429a 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SelectLanguage.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SelectLanguage.tsx
@@ -5,18 +5,14 @@ import { useAppContext } from '../../renderer/context';
import { messages } from '../../shared/gettext';
import { useHistory } from '../lib/history';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import { AriaInputGroup } from './AriaGroup';
import Selector, { SelectorItem } from './cell/Selector';
import { CustomScrollbarsRef } from './CustomScrollbars';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
const StyledSelector = styled(Selector)({
@@ -56,16 +52,12 @@ export default function SelectLanguage() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('select-language-nav', 'Select language')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('select-language-nav', 'Select language')
+ }
+ />
<NavigationScrollbars ref={scrollView}>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Settings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Settings.tsx
index f2c4c2760a..1eb7253533 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Settings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Settings.tsx
@@ -7,6 +7,7 @@ import { Button, TitleBig } from '../lib/components';
import { useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import * as Cell from './cell';
import { BackAction } from './KeyboardNavigation';
import {
@@ -19,7 +20,7 @@ import {
SettingsNavigationScrollbars,
SettingsStack,
} from './Layout';
-import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
import SettingsHeader from './SettingsHeader';
export default function Support() {
@@ -37,16 +38,12 @@ export default function Support() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('navigation-bar', 'Settings')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('navigation-bar', 'Settings')
+ }
+ />
<SettingsNavigationScrollbars fillContainer>
<SettingsContent>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SettingsImport.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SettingsImport.tsx
index 8b7169ab06..b2ebe680d8 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SettingsImport.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SettingsImport.tsx
@@ -13,17 +13,12 @@ import { RoutePath } from '../lib/routes';
import { useBoolean, useEffectEvent } from '../lib/utility-hooks';
import settingsImportActions from '../redux/settings-import/actions';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import { measurements, normalText, tinyText } from './common-styles';
import ImageView from './ImageView';
import { BackAction } from './KeyboardNavigation';
import { Footer, Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
-import {
- NavigationBar,
- NavigationInfoButton,
- NavigationItems,
- TitleBarItem,
-} from './NavigationBar';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
import { SmallButton, SmallButtonColor, SmallButtonGrid } from './SmallButton';
@@ -116,34 +111,31 @@ export default function SettingsImport() {
<BackAction action={history.pop}>
<Layout>
<SettingsContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar. This is for a feature that lets
- // TRANSLATORS: users import server IP settings.
- messages.pgettext('settings-import', 'Server IP override')
- }
- </TitleBarItem>
- <NavigationInfoButton
- title={messages.pgettext('settings-import', 'Server IP override')}
- message={[
- messages.pgettext(
- 'settings-import',
- 'On some networks, where various types of censorship are being used, our server IP addresses are sometimes blocked.',
- ),
- messages.pgettext(
- 'settings-import',
- 'To circumvent this you can import a file or a text, provided by our support team, with new IP addresses that override the default addresses of the servers in the Select location view.',
- ),
- messages.pgettext(
- 'settings-import',
- 'If you are having issues connecting to VPN servers, please contact support.',
- ),
- ]}
- />
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar. This is for a feature that lets
+ // TRANSLATORS: users import server IP settings.
+ messages.pgettext('settings-import', 'Server IP override')
+ }>
+ <AppNavigationHeader.InfoButton
+ title={messages.pgettext('settings-import', 'Server IP override')}
+ variant="secondary"
+ message={[
+ messages.pgettext(
+ 'settings-import',
+ 'On some networks, where various types of censorship are being used, our server IP addresses are sometimes blocked.',
+ ),
+ messages.pgettext(
+ 'settings-import',
+ 'To circumvent this you can import a file or a text, provided by our support team, with new IP addresses that override the default addresses of the servers in the Select location view.',
+ ),
+ messages.pgettext(
+ 'settings-import',
+ 'If you are having issues connecting to VPN servers, please contact support.',
+ ),
+ ]}
+ />
+ </AppNavigationHeader>
<Flex $flexDirection="column" $flex={1}>
<SettingsHeader>
<HeaderTitle>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SettingsTextImport.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SettingsTextImport.tsx
index c6cb325b0c..ed2dd84695 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SettingsTextImport.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SettingsTextImport.tsx
@@ -8,10 +8,9 @@ import { useHistory } from '../lib/history';
import { useCombinedRefs, useRefCallback, useStyledRef } from '../lib/utility-hooks';
import settingsImportActions from '../redux/settings-import/actions';
import { useSelector } from '../redux/store';
-import ImageView from './ImageView';
+import { AppNavigationHeader } from './';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
-import { NavigationBar, NavigationBarButton, NavigationItems, TitleBarItem } from './NavigationBar';
const StyledTextArea = styled.textarea({
width: '100%',
@@ -54,25 +53,18 @@ export default function SettingsTextImport() {
<BackAction action={back}>
<Layout>
<SettingsContainer>
- <NavigationBar alwaysDisplayBarTitle>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('settings-import', 'Import via text')
- }
- </TitleBarItem>
- <NavigationBarButton onClick={save} aria-label={messages.gettext('Save')}>
- <ImageView
- source="icon-check"
- tintColor={colors.white40}
- tintHoverColor={colors.white60}
- height={24}
- width={24}
- />
- </NavigationBarButton>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('settings-import', 'Import via text')
+ }
+ titleVisible>
+ <AppNavigationHeader.IconButton
+ icon="icon-check"
+ onClick={save}
+ aria-label={messages.gettext('Save')}
+ />
+ </AppNavigationHeader>
<StyledTextArea ref={combinedTextAreaRef} />
</SettingsContainer>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Shadowsocks.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Shadowsocks.tsx
index 4c59bde50e..b23fe817fc 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Shadowsocks.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Shadowsocks.tsx
@@ -8,18 +8,14 @@ import { removeNonNumericCharacters } from '../../shared/string-helpers';
import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import { AriaDescription, AriaInputGroup } from './AriaGroup';
import * as Cell from './cell';
import { SelectorItem, SelectorWithCustomItem } from './cell/Selector';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
const PORTS: Array<SelectorItem<number>> = [];
@@ -44,16 +40,12 @@ export default function Shadowsocks() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('wireguard-settings-nav', 'Shadowsocks')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('wireguard-settings-nav', 'Shadowsocks')
+ }
+ />
<NavigationScrollbars>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
index cab859b8c9..849b420cd1 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
@@ -16,6 +16,7 @@ import { useHistory } from '../lib/history';
import { formatHtml } from '../lib/html-formatter';
import { useEffectEvent, useStyledRef } from '../lib/utility-hooks';
import { IReduxState } from '../redux/store';
+import { AppNavigationHeader } from './';
import Accordion from './Accordion';
import * as AppButton from './AppButton';
import * as Cell from './cell';
@@ -26,7 +27,7 @@ import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
import List from './List';
import { ModalAlert, ModalAlertType } from './Modal';
-import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
import {
StyledActionIcon,
@@ -60,11 +61,7 @@ export default function SplitTunneling() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>{strings.splitTunneling}</TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader title={strings.splitTunneling} />
<StyledNavigationScrollbars ref={scrollbarsRef}>
<PlatformSpecificSplitTunnelingSettings
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx
index 554f2a2a11..98438116c9 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx
@@ -6,7 +6,7 @@ import * as AppButton from './AppButton';
import * as Cell from './cell';
import { measurements, normalText } from './common-styles';
import ImageView from './ImageView';
-import { NavigationScrollbars } from './NavigationBar';
+import { NavigationScrollbars } from './NavigationScrollbars';
import SearchBar from './SearchBar';
import { SmallButton } from './SmallButton';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Support.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Support.tsx
index 2785883ecf..34f49a1c30 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Support.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Support.tsx
@@ -7,6 +7,7 @@ import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import {
AriaDescribed,
AriaDescription,
@@ -18,13 +19,8 @@ import {
import * as Cell from './cell';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
const StyledContent = styled.div({
@@ -42,16 +38,12 @@ export default function Support() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('support-view', 'Support')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('support-view', 'Support')
+ }
+ />
<NavigationScrollbars>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx
index 42c5a61373..23a5d68b34 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx
@@ -13,13 +13,13 @@ import { formatHtml } from '../lib/html-formatter';
import { RoutePath } from '../lib/routes';
import { useBoolean } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
+import { AppMainHeader } from './app-main-header';
import * as AppButton from './AppButton';
import * as Cell from './cell';
import { bigText, measurements, normalText, tinyText } from './common-styles';
import CustomScrollbars from './CustomScrollbars';
-import { Brand, HeaderBarSettingsButton } from './HeaderBar';
import ImageView from './ImageView';
-import { Footer, Header, Layout, SettingsContainer } from './Layout';
+import { Footer, Layout, SettingsContainer } from './Layout';
import List from './List';
import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal';
@@ -124,10 +124,9 @@ export default function TooManyDevices() {
return (
<ModalContainer>
<Layout>
- <Header>
- <Brand />
- <HeaderBarSettingsButton />
- </Header>
+ <AppMainHeader>
+ <AppMainHeader.SettingsButton />
+ </AppMainHeader>
<StyledCustomScrollbars fillContainer>
<StyledContainer>
<StyledBody>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/UdpOverTcp.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/UdpOverTcp.tsx
index 7179daca77..c6e6ff43f6 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/UdpOverTcp.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/UdpOverTcp.tsx
@@ -6,19 +6,15 @@ import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import { AriaInputGroup } from './AriaGroup';
import * as Cell from './cell';
import Selector, { SelectorItem } from './cell/Selector';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
import { ModalMessage } from './Modal';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
const UDP2TCP_PORTS = [80, 5001];
@@ -46,16 +42,12 @@ export default function UdpOverTcp() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('wireguard-settings-nav', 'UDP-over-TCP')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('wireguard-settings-nav', 'UDP-over-TCP')
+ }
+ />
<NavigationScrollbars>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/UserInterfaceSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/UserInterfaceSettings.tsx
index 38a0f71bbd..13c29a7ea7 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/UserInterfaceSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/UserInterfaceSettings.tsx
@@ -6,6 +6,7 @@ import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
import { BackAction } from './KeyboardNavigation';
@@ -17,13 +18,8 @@ import {
SettingsGroup,
SettingsStack,
} from './Layout';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
const StyledAnimateMapCellGroup = styled(SettingsGroup)({
@@ -41,16 +37,12 @@ export default function UserInterfaceSettings() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('user-interface-settings-view', 'User interface settings')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('user-interface-settings-view', 'User interface settings')
+ }
+ />
<NavigationScrollbars>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx
index 5754ebce72..7a21a18f6b 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx
@@ -16,25 +16,23 @@ import { RoutePath } from '../lib/routes';
import { useBoolean } from '../lib/utility-hooks';
import { RelaySettingsRedux } from '../redux/settings/reducers';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import * as AppButton from './AppButton';
import { AriaDescription, AriaDetails, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
import Selector, { SelectorItem } from './cell/Selector';
import CustomDnsSettings from './CustomDnsSettings';
-import InfoButton, { InfoIcon } from './InfoButton';
+import InfoButton from './InfoButton';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer, SettingsContent, SettingsGroup, SettingsStack } from './Layout';
import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-const StyledInfoIcon = styled(InfoIcon)({
+const StyledInfoButton = styled(InfoButton).attrs({
+ size: 'small',
+})({
marginRight: Spacings.spacing5,
});
@@ -63,16 +61,12 @@ export default function VpnSettings() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('vpn-settings-view', 'VPN settings')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('vpn-settings-view', 'VPN settings')
+ }
+ />
<NavigationScrollbars>
<SettingsHeader>
@@ -204,7 +198,7 @@ function AllowLan() {
</Cell.InputLabel>
</AriaLabel>
<AriaDetails>
- <InfoButton>
+ <StyledInfoButton>
<ModalMessage>
{messages.pgettext(
'vpn-settings-view',
@@ -225,7 +219,7 @@ function AllowLan() {
<li>fc00::/7</li>
</LanIpRanges>
</ModalMessage>
- </InfoButton>
+ </StyledInfoButton>
</AriaDetails>
<AriaInput>
<Cell.Switch isOn={allowLan} onChange={setAllowLan} />
@@ -263,7 +257,7 @@ function DnsBlockers() {
<StyledTitleLabel as="label" disabled={dns.state === 'custom'}>
{messages.pgettext('vpn-settings-view', 'DNS content blockers')}
</StyledTitleLabel>
- <InfoButton>
+ <StyledInfoButton>
<ModalMessage>
{messages.pgettext(
'vpn-settings-view',
@@ -287,7 +281,7 @@ function DnsBlockers() {
),
)}
</ModalMessage>
- </InfoButton>
+ </StyledInfoButton>
</>
);
@@ -368,14 +362,14 @@ function BlockMalware() {
</IndentedValueLabel>
</AriaLabel>
<AriaDetails>
- <InfoButton>
+ <StyledInfoButton>
<ModalMessage>
{messages.pgettext(
'vpn-settings-view',
'Warning: The malware blocker is not an anti-virus and should not be treated as such, this is just an extra layer of protection.',
)}
</ModalMessage>
- </InfoButton>
+ </StyledInfoButton>
</AriaDetails>
<AriaInput>
<Cell.Switch
@@ -510,7 +504,7 @@ function EnableIpv6() {
<Cell.InputLabel>{messages.pgettext('vpn-settings-view', 'Enable IPv6')}</Cell.InputLabel>
</AriaLabel>
<AriaDetails>
- <InfoButton>
+ <StyledInfoButton>
<ModalMessage>
{messages.pgettext(
'vpn-settings-view',
@@ -523,7 +517,7 @@ function EnableIpv6() {
'IPv4 is always enabled and the majority of websites and applications use this protocol. We do not recommend enabling IPv6 unless you know you need it.',
)}
</ModalMessage>
- </InfoButton>
+ </StyledInfoButton>
</AriaDetails>
<AriaInput>
<Cell.Switch isOn={enableIpv6} onChange={setEnableIpv6} />
@@ -538,19 +532,19 @@ function KillSwitchInfo() {
return (
<>
- <Cell.CellButton onClick={showKillSwitchInfo}>
- <AriaInputGroup>
+ <AriaInputGroup>
+ <Cell.Container>
<AriaLabel>
<Cell.InputLabel>
{messages.pgettext('vpn-settings-view', 'Kill switch')}
</Cell.InputLabel>
</AriaLabel>
- <StyledInfoIcon />
+ <StyledInfoButton onClick={showKillSwitchInfo} />
<AriaInput>
<Cell.Switch isOn disabled />
</AriaInput>
- </AriaInputGroup>
- </Cell.CellButton>
+ </Cell.Container>
+ </AriaInputGroup>
<ModalAlert
isOpen={killSwitchInfoVisible}
type={ModalAlertType.info}
@@ -622,7 +616,7 @@ function LockdownMode() {
</Cell.InputLabel>
</AriaLabel>
<AriaDetails>
- <InfoButton>
+ <StyledInfoButton>
<ModalMessage>
{messages.pgettext(
'vpn-settings-view',
@@ -635,7 +629,7 @@ function LockdownMode() {
'With Lockdown Mode enabled, you must be connected to a Mullvad VPN server to be able to reach the internet. Manually disconnecting or quitting the app will block your connection.',
)}
</ModalMessage>
- </InfoButton>
+ </StyledInfoButton>
</AriaDetails>
<AriaInput>
<Cell.Switch isOn={blockWhenDisconnected} onChange={setLockDownMode} />
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/WireguardSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/WireguardSettings.tsx
index 5a87ffdb8b..450ced9d31 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/WireguardSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/WireguardSettings.tsx
@@ -17,19 +17,15 @@ import { useRelaySettingsUpdater } from '../lib/constraint-updater';
import { useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { useSelector } from '../redux/store';
+import { AppNavigationHeader } from './';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
import Selector, { SelectorItem, SelectorWithCustomItem } from './cell/Selector';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer, SettingsContent, SettingsGroup, SettingsStack } from './Layout';
import { ModalMessage } from './Modal';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from './NavigationBar';
+import { NavigationContainer } from './NavigationContainer';
+import { NavigationScrollbars } from './NavigationScrollbars';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
const MIN_WIREGUARD_MTU_VALUE = 1280;
@@ -52,19 +48,15 @@ export default function WireguardSettings() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {sprintf(
- // TRANSLATORS: Title label in navigation bar
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
- messages.pgettext('wireguard-settings-nav', '%(wireguard)s settings'),
- { wireguard: strings.wireguard },
- )}
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={sprintf(
+ // TRANSLATORS: Title label in navigation bar
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
+ messages.pgettext('wireguard-settings-nav', '%(wireguard)s settings'),
+ { wireguard: strings.wireguard },
+ )}
+ />
<NavigationScrollbars>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/AppMainHeader.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/AppMainHeader.tsx
new file mode 100644
index 0000000000..cae1bf4336
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/AppMainHeader.tsx
@@ -0,0 +1,80 @@
+import { TunnelState } from '../../../shared/daemon-rpc-types';
+import { Flex, HeaderProps, Logo, LogoProps, MainHeader } from '../../lib/components';
+import { Spacings } from '../../lib/foundations';
+import { useSelector } from '../../redux/store';
+import { FocusFallback } from '../Focus';
+import {
+ AppMainHeaderBarAccountButton,
+ AppMainHeaderDeviceInfo,
+ AppMainHeaderSettingsButton,
+} from './components';
+
+export interface MainHeaderProps extends Omit<HeaderProps, 'variant' | 'size'> {
+ variant?: HeaderProps['variant'] | 'basedOnConnectionStatus';
+ size?: HeaderProps['size'] | 'basedOnLoginStatus';
+ logoVariant?: LogoProps['variant'] | 'none';
+ children?: React.ReactNode;
+}
+
+const AppMainHeader = ({
+ logoVariant = 'both',
+ variant: variantProp,
+ size: sizeProp,
+ children,
+ ...props
+}: MainHeaderProps) => {
+ const connectionStatus = useSelector((state) => state.connection.status);
+
+ const variant =
+ variantProp === 'basedOnConnectionStatus'
+ ? getVariantByTunnelState(connectionStatus)
+ : variantProp;
+
+ const loggedIn = useSelector((state) => state.account.status.type === 'ok');
+ const size = sizeProp === 'basedOnLoginStatus' ? (loggedIn ? '2' : '1') : sizeProp;
+
+ return (
+ <MainHeader variant={variant} size={size} {...props}>
+ <Flex $justifyContent="space-between">
+ <FocusFallback>
+ {logoVariant !== 'none' ? <Logo variant={logoVariant} /> : <div />}
+ </FocusFallback>
+ <Flex $gap={Spacings.spacing5} $alignItems="center">
+ {children}
+ </Flex>
+ </Flex>
+ {size == '2' && (
+ <Flex $alignItems="flex-end">
+ <AppMainHeaderDeviceInfo />
+ </Flex>
+ )}
+ </MainHeader>
+ );
+};
+
+const AppMainHeaderNamespace = Object.assign(AppMainHeader, {
+ AccountButton: AppMainHeaderBarAccountButton,
+ SettingsButton: AppMainHeaderSettingsButton,
+});
+
+export { AppMainHeaderNamespace as AppMainHeader };
+
+const getVariantByTunnelState = (tunnelState: TunnelState): HeaderProps['variant'] => {
+ switch (tunnelState.state) {
+ case 'disconnected':
+ return 'error';
+ case 'connecting':
+ case 'connected':
+ return 'success';
+ case 'error':
+ return !tunnelState.details.blockingError ? 'success' : 'error';
+ case 'disconnecting':
+ switch (tunnelState.details) {
+ case 'block':
+ case 'reconnect':
+ return 'success';
+ case 'nothing':
+ return 'error';
+ }
+ }
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderAccountButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderAccountButton.tsx
new file mode 100644
index 0000000000..f66ba6d17f
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderAccountButton.tsx
@@ -0,0 +1,32 @@
+import { useCallback } from 'react';
+
+import { messages } from '../../../../shared/gettext';
+import { IconButtonProps, MainHeader } from '../../../lib/components';
+import { transitions, useHistory } from '../../../lib/history';
+import { RoutePath } from '../../../lib/routes';
+import { useSelector } from '../../../redux/store';
+
+export type MainHeaderBarAccountButtonProps = Omit<IconButtonProps, 'icon'>;
+
+export const AppMainHeaderBarAccountButton = (props: MainHeaderBarAccountButtonProps) => {
+ const history = useHistory();
+ const openAccount = useCallback(
+ () => history.push(RoutePath.account, { transition: transitions.show }),
+ [history],
+ );
+
+ const loggedIn = useSelector((state) => state.account.status.type === 'ok');
+ if (!loggedIn) {
+ return null;
+ }
+
+ return (
+ <MainHeader.IconButton
+ icon="icon-account"
+ onClick={openAccount}
+ data-testid="account-button"
+ aria-label={messages.gettext('Account settings')}
+ {...props}
+ />
+ );
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderDeviceInfo.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderDeviceInfo.tsx
new file mode 100644
index 0000000000..d13e486455
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderDeviceInfo.tsx
@@ -0,0 +1,54 @@
+import { sprintf } from 'sprintf-js';
+import styled from 'styled-components';
+
+import { closeToExpiry, formatRemainingTime, hasExpired } from '../../../../shared/account-expiry';
+import { messages } from '../../../../shared/gettext';
+import { capitalizeEveryWord } from '../../../../shared/string-helpers';
+import { Flex, FootnoteMini } from '../../../lib/components';
+import { Colors, Spacings } from '../../../lib/foundations';
+import { useSelector } from '../../../redux/store';
+
+const StyledTimeLeftLabel = styled(FootnoteMini)({
+ whiteSpace: 'nowrap',
+});
+
+const StyledDeviceLabel = styled(FootnoteMini)({
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+});
+
+export const AppMainHeaderDeviceInfo = () => {
+ const deviceName = useSelector((state) => state.account.deviceName);
+ const accountExpiry = useSelector((state) => state.account.expiry);
+ const isOutOfTime = accountExpiry ? hasExpired(accountExpiry) : false;
+ const formattedExpiry = isOutOfTime
+ ? sprintf(messages.ngettext('1 day', '%d days', 0), 0)
+ : accountExpiry
+ ? formatRemainingTime(accountExpiry)
+ : '';
+
+ return (
+ <Flex $gap={Spacings.spacing6} $margin={{ top: Spacings.spacing1 }}>
+ <StyledDeviceLabel color={Colors.white80}>
+ {sprintf(
+ // TRANSLATORS: A label that will display the newly created device name to inform the user
+ // TRANSLATORS: about it.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(deviceName)s - The name of the current device
+ messages.pgettext('device-management', 'Device name: %(deviceName)s'),
+ {
+ deviceName: capitalizeEveryWord(deviceName ?? ''),
+ },
+ )}
+ </StyledDeviceLabel>
+ {accountExpiry && !closeToExpiry(accountExpiry) && !isOutOfTime && (
+ <StyledTimeLeftLabel color={Colors.white80}>
+ {sprintf(messages.pgettext('device-management', 'Time left: %(timeLeft)s'), {
+ timeLeft: formattedExpiry,
+ })}
+ </StyledTimeLeftLabel>
+ )}
+ </Flex>
+ );
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderSettingsButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderSettingsButton.tsx
new file mode 100644
index 0000000000..d738f502a3
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/AppMainHeaderSettingsButton.tsx
@@ -0,0 +1,26 @@
+import { useCallback } from 'react';
+
+import { messages } from '../../../../shared/gettext';
+import { IconButtonProps, MainHeader } from '../../../lib/components';
+import { transitions, useHistory } from '../../../lib/history';
+import { RoutePath } from '../../../lib/routes';
+
+export type MainHeaderSettingsButtonProps = Omit<IconButtonProps, 'icon'>;
+
+export function AppMainHeaderSettingsButton(props: MainHeaderSettingsButtonProps) {
+ const history = useHistory();
+
+ const openSettings = useCallback(() => {
+ if (!props.disabled) {
+ history.push(RoutePath.settings, { transition: transitions.show });
+ }
+ }, [history, props.disabled]);
+
+ return (
+ <MainHeader.IconButton
+ icon="icon-settings"
+ onClick={openSettings}
+ aria-label={messages.gettext('Settings')}
+ />
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/index.ts
new file mode 100644
index 0000000000..2ad145e283
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/components/index.ts
@@ -0,0 +1,3 @@
+export * from './AppMainHeaderDeviceInfo';
+export * from './AppMainHeaderAccountButton';
+export * from './AppMainHeaderSettingsButton';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/index.ts
new file mode 100644
index 0000000000..a4b73cc7c3
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/app-main-header/index.ts
@@ -0,0 +1 @@
+export * from './AppMainHeader';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/AppNavigationHeader.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/AppNavigationHeader.tsx
new file mode 100644
index 0000000000..80f1f794f4
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/AppNavigationHeader.tsx
@@ -0,0 +1,30 @@
+import { useContext } from 'react';
+
+import { NavigationHeader, NavigationHeaderProps } from '../../lib/components';
+import { NavigationScrollContext } from '../NavigationContainer';
+import { AppNavigationHeaderBackButton, AppNavigationHeaderInfoButton } from './components';
+
+export interface NavigationBarProps extends NavigationHeaderProps {
+ title?: string;
+ children?: React.ReactNode;
+}
+
+const AppNavigationHeader = ({ title, children, ...props }: NavigationBarProps) => {
+ const { showsBarTitle } = useContext(NavigationScrollContext);
+ return (
+ <NavigationHeader titleVisible={showsBarTitle} {...props}>
+ <AppNavigationHeaderBackButton />
+ {title && <NavigationHeader.Title>{title}</NavigationHeader.Title>}
+ <NavigationHeader.ButtonGroup $justifyContent="flex-end">
+ {children}
+ </NavigationHeader.ButtonGroup>
+ </NavigationHeader>
+ );
+};
+
+const AppNavigationHeaderNamespace = Object.assign(AppNavigationHeader, {
+ IconButton: NavigationHeader.IconButton,
+ InfoButton: AppNavigationHeaderInfoButton,
+});
+
+export { AppNavigationHeaderNamespace as AppNavigationHeader };
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/AppNavigationHeaderBackButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/AppNavigationHeaderBackButton.tsx
new file mode 100644
index 0000000000..686305180a
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/AppNavigationHeaderBackButton.tsx
@@ -0,0 +1,32 @@
+import { useContext, useMemo } from 'react';
+
+import { messages } from '../../../../shared/gettext';
+import { IconButton } from '../../../lib/components';
+import { transitions, useHistory } from '../../../lib/history';
+import { BackActionContext } from '../../KeyboardNavigation';
+
+export const AppNavigationHeaderBackButton = () => {
+ const history = useHistory();
+ // Compare the transition name with dismiss to infer wheter or not the view will slide
+ // horizontally or vertically and then use matching button.
+ const backIcon = useMemo(
+ () => history.getPopTransition().name !== transitions.dismiss.name,
+ [history],
+ );
+ const { parentBackAction } = useContext(BackActionContext);
+
+ if (!parentBackAction) return null;
+
+ const iconSource = backIcon ? 'icon-back' : 'icon-close-down';
+ const ariaLabel = backIcon ? messages.gettext('Back') : messages.gettext('Close');
+
+ return (
+ <IconButton
+ variant="secondary"
+ size="regular"
+ icon={iconSource}
+ aria-label={ariaLabel}
+ onClick={parentBackAction}
+ />
+ );
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/AppNavigationHeaderInfoButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/AppNavigationHeaderInfoButton.tsx
new file mode 100644
index 0000000000..882308e454
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/AppNavigationHeaderInfoButton.tsx
@@ -0,0 +1,5 @@
+import InfoButton, { InfoButtonProps } from '../../InfoButton';
+
+export const AppNavigationHeaderInfoButton = (props: InfoButtonProps) => {
+ return <InfoButton size="regular" variant="secondary" {...props} />;
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/index.ts
new file mode 100644
index 0000000000..6a81375ace
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/components/index.ts
@@ -0,0 +1,2 @@
+export * from './AppNavigationHeaderBackButton';
+export * from './AppNavigationHeaderInfoButton';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/index.ts
new file mode 100644
index 0000000000..5bf59572e3
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/app-navigation-header/index.ts
@@ -0,0 +1 @@
+export * from './AppNavigationHeader';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/cell/Selector.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/cell/Selector.tsx
index b1e20a0c41..4fc8e15925 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/cell/Selector.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/cell/Selector.tsx
@@ -3,6 +3,7 @@ import styled from 'styled-components';
import { colors } from '../../../config.json';
import { messages } from '../../../shared/gettext';
+import { Spacings } from '../../lib/foundations';
import { useHistory } from '../../lib/history';
import { RoutePath } from '../../lib/routes';
import { useStyledRef } from '../../lib/utility-hooks';
@@ -15,6 +16,10 @@ const StyledTitleLabel = styled(Cell.SectionTitle)({
flex: 1,
});
+const StyledInfoButton = styled(InfoButton)({
+ marginRight: Spacings.spacing5,
+});
+
export interface SelectorItem<T> {
label: string;
value: T;
@@ -94,7 +99,9 @@ export default function Selector<T, U>(props: SelectorProps<T, U>) {
</AriaLabel>
{props.details && (
<AriaDetails>
- <InfoButton title={props.infoTitle}>{props.details}</InfoButton>
+ <StyledInfoButton title={props.infoTitle} size="small">
+ {props.details}
+ </StyledInfoButton>
</AriaDetails>
)}
</>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsGroup.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsGroup.tsx
index c1b9ac80d1..ccca7cdc0b 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsGroup.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsGroup.tsx
@@ -85,7 +85,7 @@ export function SettingsGroup(props: React.PropsWithChildren<SettingsGroupProps>
<StyledTitle>
{props.title}
{props.infoMessage !== undefined && (
- <StyledInfoButton size={12} message={props.infoMessage} />
+ <StyledInfoButton size="small" message={props.infoMessage} />
)}
</StyledTitle>
)}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/index.ts
new file mode 100644
index 0000000000..612a0df28f
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/index.ts
@@ -0,0 +1 @@
+export * from './app-navigation-header';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/main-view/MainView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/main-view/MainView.tsx
index 9327094a51..06a4ec8e69 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/main-view/MainView.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/main-view/MainView.tsx
@@ -1,7 +1,7 @@
import styled from 'styled-components';
import { useSelector } from '../../redux/store';
-import { calculateHeaderBarStyle, DefaultHeaderBar } from '../HeaderBar';
+import { AppMainHeader } from '../app-main-header';
import ImageView from '../ImageView';
import { Container, Layout } from '../Layout';
import Map from '../Map';
@@ -49,7 +49,10 @@ export default function MainView() {
return (
<Layout>
- <DefaultHeaderBar barStyle={calculateHeaderBarStyle(connection.status)} />
+ <AppMainHeader size="basedOnLoginStatus" variant="basedOnConnectionStatus">
+ <AppMainHeader.AccountButton />
+ <AppMainHeader.SettingsButton />
+ </AppMainHeader>
<StyledContainer>
<Map />
<Content>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx
index 32c516d0a0..7779d6fb2b 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx
@@ -4,6 +4,7 @@ import { sprintf } from 'sprintf-js';
import { colors, strings } from '../../../config.json';
import { Ownership } from '../../../shared/daemon-rpc-types';
import { messages } from '../../../shared/gettext';
+import { IconButton } from '../../lib/components';
import { useRelaySettingsUpdater } from '../../lib/constraint-updater';
import { daitaFilterActive, filterSpecialLocations } from '../../lib/filter-locations';
import { useHistory } from '../../lib/history';
@@ -11,19 +12,14 @@ import { formatHtml } from '../../lib/html-formatter';
import { useNormalRelaySettings } from '../../lib/relay-settings-hooks';
import { RoutePath } from '../../lib/routes';
import { useSelector } from '../../redux/store';
+import { AppNavigationHeader } from '../';
import * as Cell from '../cell';
import { useFilteredProviders } from '../Filter';
import ImageView from '../ImageView';
import { BackAction } from '../KeyboardNavigation';
import { Layout, SettingsContainer } from '../Layout';
-import {
- NavigationBar,
- NavigationBarButton,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from '../NavigationBar';
+import { NavigationContainer } from '../NavigationContainer';
+import { NavigationScrollbars } from '../NavigationScrollbars';
import CombinedLocationList, { CombinedLocationListProps } from './CombinedLocationList';
import CustomLists from './CustomLists';
import { useRelayListContext } from './RelayListContext';
@@ -133,26 +129,19 @@ export default function SelectLocation() {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar alwaysDisplayBarTitle>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('select-location-nav', 'Select location')
- }
- </TitleBarItem>
-
- <NavigationBarButton onClick={onViewFilter} aria-label={messages.gettext('Filter')}>
- <ImageView
- source="icon-filter-round"
- tintColor={colors.white40}
- tintHoverColor={colors.white60}
- height={24}
- width={24}
- />
- </NavigationBarButton>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader
+ title={
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('select-location-nav', 'Select location')
+ }
+ titleVisible>
+ <IconButton
+ icon="icon-filter-round"
+ variant="secondary"
+ onClick={onViewFilter}
+ aria-label={messages.gettext('Filter')}
+ />
+ </AppNavigationHeader>
<StyledNavigationBarAttachment>
{allowEntrySelection && (
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SpecialLocationList.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SpecialLocationList.tsx
index e4d105d635..552f86d9fc 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SpecialLocationList.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SpecialLocationList.tsx
@@ -35,7 +35,9 @@ export default function SpecialLocationList<T>({ source, ...props }: SpecialLoca
);
}
-const StyledSpecialLocationInfoButton = styled(InfoButton)({ padding: '0 25px', margin: 0 });
+const StyledSpecialLocationInfoButton = styled(InfoButton).attrs({
+ size: 'small',
+})({ width: '56px', height: '48px' });
const StyledSpecialLocationSideButton = styled(ImageView)({ padding: '0 3px' });
interface SpecialLocationRowProps<T> {
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/AppInfoView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/AppInfoView.tsx
index 41671c46a9..dedf6b72bb 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/AppInfoView.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/AppInfoView.tsx
@@ -1,14 +1,10 @@
import { messages } from '../../../../shared/gettext';
import { useHistory } from '../../../lib/history';
+import { AppNavigationHeader } from '../../';
import { BackAction } from '../../KeyboardNavigation';
import { Layout, SettingsContainer, SettingsGroup, SettingsStack } from '../../Layout';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from '../../NavigationBar';
+import { NavigationContainer } from '../../NavigationContainer';
+import { NavigationScrollbars } from '../../NavigationScrollbars';
import SettingsHeader, { HeaderTitle } from '../../SettingsHeader';
import { AppVersionListItem, ChangelogListItem } from './components';
@@ -19,11 +15,7 @@ export const AppInfoView = () => {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>{messages.pgettext('app-info-view', 'App info')}</TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader title={messages.pgettext('app-info-view', 'App info')} />
<NavigationScrollbars>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/changelog/ChangelogView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/changelog/ChangelogView.tsx
index 4a93908e65..19a499a34d 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/views/changelog/ChangelogView.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/changelog/ChangelogView.tsx
@@ -4,21 +4,16 @@ import styled from 'styled-components';
import { links } from '../../../../config.json';
import { messages } from '../../../../shared/gettext';
import { useAppContext } from '../../../context';
-import { BodySmall, Button, Flex, TitleBig, TitleLarge } from '../../../lib/components';
-import { Container } from '../../../lib/components';
+import { BodySmall, Button, Container, Flex, TitleBig, TitleLarge } from '../../../lib/components';
import { Colors, Spacings } from '../../../lib/foundations';
import { useHistory } from '../../../lib/history';
import { useSelector } from '../../../redux/store';
+import { AppNavigationHeader } from '../../';
import ImageView from '../../ImageView';
import { BackAction } from '../../KeyboardNavigation';
import { Layout, SettingsContainer } from '../../Layout';
-import {
- NavigationBar,
- NavigationContainer,
- NavigationItems,
- NavigationScrollbars,
- TitleBarItem,
-} from '../../NavigationBar';
+import { NavigationContainer } from '../../NavigationContainer';
+import { NavigationScrollbars } from '../../NavigationScrollbars';
import SettingsHeader from '../../SettingsHeader';
const StyledList = styled(Flex)({
@@ -49,11 +44,7 @@ export const ChangelogView = () => {
<Layout>
<SettingsContainer>
<NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>{messages.pgettext('changelog-view', 'What’s new')}</TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ <AppNavigationHeader title={messages.pgettext('changelog-view', 'What’s new')} />
<NavigationScrollbars>
<SettingsHeader>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/atoms/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/atoms/index.ts
new file mode 100644
index 0000000000..95c029cdb9
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/atoms/index.ts
@@ -0,0 +1 @@
+export * from './logo/Logo';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/atoms/logo/Logo.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/atoms/logo/Logo.tsx
new file mode 100644
index 0000000000..edc0c5aec3
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/atoms/logo/Logo.tsx
@@ -0,0 +1,41 @@
+import ImageView from '../../../../components/ImageView';
+import { Spacings } from '../../../foundations';
+import { Flex } from '../../layout';
+
+export interface LogoProps {
+ variant?: 'icon' | 'text' | 'both';
+ size?: '1' | '2';
+}
+
+const logoSizes = {
+ '1': 38,
+ '2': 106,
+};
+
+const textSizes = {
+ '1': 15.4,
+ '2': 18,
+};
+
+export const Logo = ({ variant = 'icon', size: sizeProp = '1' }: LogoProps) => {
+ switch (variant) {
+ case 'icon': {
+ const logoSize = logoSizes[sizeProp];
+ return <ImageView source="logo-icon" height={logoSize} />;
+ }
+ case 'text': {
+ const textSize = textSizes[sizeProp];
+ return <ImageView source="logo-text" height={textSize} />;
+ }
+ case 'both': {
+ const logoSize = logoSizes[sizeProp];
+ const textSize = textSizes[sizeProp];
+ return (
+ <Flex $flex={1} $alignItems="center" $gap={Spacings.spacing3}>
+ <ImageView source="logo-icon" height={logoSize} />
+ <ImageView source="logo-text" height={textSize} />
+ </Flex>
+ );
+ }
+ }
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/index.ts
index 54e8908581..8a398fe7a2 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/index.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/index.ts
@@ -1,3 +1,5 @@
export * from './layout';
+export * from './atoms';
export * from './typography';
export * from './molecules';
+export * from './organisms';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/molecules/Button.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/molecules/Button.tsx
index 0721fbf54a..858ecee82b 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/molecules/Button.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/molecules/Button.tsx
@@ -60,7 +60,16 @@ const StyledButton = styled.button({
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
- { variant = 'primary', size = 'regular', leading, trailing, children, disabled, ...props },
+ {
+ variant = 'primary',
+ size = 'regular',
+ leading,
+ trailing,
+ children,
+ disabled,
+ style,
+ ...props
+ },
ref,
) => {
const styles = variants[variant];
@@ -73,6 +82,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
'--hover': styles.hover,
'--disabled': styles.disabled,
'--size': sizes[size],
+ ...style,
} as React.CSSProperties
}
disabled={disabled}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/molecules/IconButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/molecules/IconButton.tsx
index d6f1dcc242..606f757fdc 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/molecules/IconButton.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/molecules/IconButton.tsx
@@ -44,11 +44,20 @@ const StyledButton = styled.button({
});
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
- ({ icon, variant = 'primary', size: sizeProp = 'regular', disabled, ...props }, ref) => {
+ ({ icon, variant = 'primary', size: sizeProp = 'regular', disabled, style, ...props }, ref) => {
const styles = variants[variant];
const size = sizes[sizeProp];
return (
- <StyledButton ref={ref} disabled={disabled} {...props}>
+ <StyledButton
+ ref={ref}
+ disabled={disabled}
+ style={
+ {
+ '--size': `${size}px`,
+ ...style,
+ } as React.CSSProperties
+ }
+ {...props}>
<ImageView
source={icon}
tintColor={styles.background}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/index.ts
new file mode 100644
index 0000000000..5b82330cdf
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/index.ts
@@ -0,0 +1,2 @@
+export * from './main-header';
+export * from './navigation-header';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/MainHeader.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/MainHeader.tsx
new file mode 100644
index 0000000000..b3cf54c9d2
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/MainHeader.tsx
@@ -0,0 +1,55 @@
+import styled from 'styled-components';
+
+import { Colors, Spacings } from '../../../foundations';
+import { TransientProps } from '../../../types';
+import { Flex } from '../../layout';
+import { MainHeaderIconButton } from './components';
+
+export type HeaderProps = React.PropsWithChildren<{
+ size?: '1' | '2';
+ variant?: 'default' | 'success' | 'error';
+}>;
+
+const sizes = {
+ '1': '68px',
+ '2': '80px',
+};
+
+const variants = {
+ default: Colors.blue,
+ error: Colors.red,
+ success: Colors.green,
+};
+
+const StyledHeader = styled.header<TransientProps<HeaderProps>>(
+ ({ $size = '1', $variant = 'default' }) => ({
+ height: sizes[$size],
+ minHeight: sizes[$size],
+
+ backgroundColor: variants[$variant],
+ transition: 'height 250ms ease-in-out, min-height 250ms ease-in-out',
+ }),
+);
+
+const MainHeader = ({ size = '1', variant = 'default', children, ...props }: HeaderProps) => {
+ return (
+ <StyledHeader $size={size} $variant={variant} {...props}>
+ <Flex
+ $flexDirection="column"
+ $justifyContent="center"
+ $margin={{
+ horizontal: Spacings.spacing5,
+ top: Spacings.spacing5,
+ bottom: Spacings.spacing3,
+ }}>
+ {children}
+ </Flex>
+ </StyledHeader>
+ );
+};
+
+const MainHeaderNamespace = Object.assign(MainHeader, {
+ IconButton: MainHeaderIconButton,
+});
+
+export { MainHeaderNamespace as MainHeader };
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/components/MainHeaderIconButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/components/MainHeaderIconButton.tsx
new file mode 100644
index 0000000000..907f48cb00
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/components/MainHeaderIconButton.tsx
@@ -0,0 +1,5 @@
+import { IconButton, IconButtonProps } from '../../../molecules';
+
+export const MainHeaderIconButton = (props: IconButtonProps) => {
+ return <IconButton variant="secondary" {...props} />;
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/components/index.ts
new file mode 100644
index 0000000000..4d837fe571
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/components/index.ts
@@ -0,0 +1 @@
+export * from './MainHeaderIconButton';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/index.ts
new file mode 100644
index 0000000000..3d8a1927af
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/main-header/index.ts
@@ -0,0 +1 @@
+export * from './MainHeader';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/NavigationHeader.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/NavigationHeader.tsx
new file mode 100644
index 0000000000..9b73e18f85
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/NavigationHeader.tsx
@@ -0,0 +1,53 @@
+import styled from 'styled-components';
+
+import { Colors, Spacings } from '../../../foundations';
+import { TransientProps } from '../../../types';
+import { Flex } from '../../layout';
+import {
+ NavigationHeaderButtonGroup,
+ NavigationHeaderIconButton,
+ NavigationHeaderTitle,
+} from './components';
+import { NavigationHeaderProvider } from './NavigationHeaderContext';
+
+export type NavigationHeaderProps = React.PropsWithChildren<{
+ titleVisible?: boolean;
+}>;
+
+const StyledHeader = styled.nav<TransientProps<NavigationHeaderProps>>({
+ backgroundColor: Colors.darkBlue,
+});
+
+export const StyledContent = styled.div({
+ display: 'grid',
+ gridTemplateColumns: '1fr auto 1fr',
+ placeContent: 'center',
+ minHeight: '32px',
+ height: '32px',
+});
+
+const NavigationHeader = ({ titleVisible, children, ...props }: NavigationHeaderProps) => {
+ return (
+ <NavigationHeaderProvider titleVisible={!!titleVisible}>
+ <StyledHeader {...props}>
+ <Flex
+ $flexDirection="column"
+ $justifyContent="center"
+ $padding={{
+ horizontal: Spacings.spacing5,
+ vertical: Spacings.spacing3,
+ }}>
+ <StyledContent>{children}</StyledContent>
+ </Flex>
+ </StyledHeader>
+ </NavigationHeaderProvider>
+ );
+};
+
+const NavigationHeaderNamespace = Object.assign(NavigationHeader, {
+ ButtonGroup: NavigationHeaderButtonGroup,
+ IconButton: NavigationHeaderIconButton,
+ Title: NavigationHeaderTitle,
+});
+
+export { NavigationHeaderNamespace as NavigationHeader };
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/NavigationHeaderContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/NavigationHeaderContext.tsx
new file mode 100644
index 0000000000..d0111b50bd
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/NavigationHeaderContext.tsx
@@ -0,0 +1,24 @@
+import { createContext, useContext } from 'react';
+
+interface NavigationHeaderContextProps {
+ titleVisible: boolean;
+}
+
+const NavigationHeaderContext = createContext<NavigationHeaderContextProps | undefined>(undefined);
+
+export const NavigationHeaderProvider = ({
+ titleVisible,
+ children,
+}: React.PropsWithChildren<NavigationHeaderContextProps>) => (
+ <NavigationHeaderContext.Provider value={{ titleVisible }}>
+ {children}
+ </NavigationHeaderContext.Provider>
+);
+
+export const useNavigationHeader = (): NavigationHeaderContextProps => {
+ const context = useContext(NavigationHeaderContext);
+ if (context === undefined) {
+ throw new Error('useNavigationHeader must be used within a NavigationHeaderProvider');
+ }
+ return context;
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderButtonGroup.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderButtonGroup.tsx
new file mode 100644
index 0000000000..b6fc317554
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderButtonGroup.tsx
@@ -0,0 +1,9 @@
+import styled from 'styled-components';
+
+import { Spacings } from '../../../../foundations';
+import { Flex } from '../../../layout';
+
+export const NavigationHeaderButtonGroup = styled(Flex).attrs({
+ $gap: Spacings.spacing6,
+ $alignItems: 'center',
+})({});
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderIconButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderIconButton.tsx
new file mode 100644
index 0000000000..531e97eafd
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderIconButton.tsx
@@ -0,0 +1,5 @@
+import { IconButton, IconButtonProps } from '../../../molecules';
+
+export const NavigationHeaderIconButton = (props: IconButtonProps) => {
+ return <IconButton variant="secondary" {...props} />;
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderTitle.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderTitle.tsx
new file mode 100644
index 0000000000..31164ffdea
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/NavigationHeaderTitle.tsx
@@ -0,0 +1,22 @@
+import styled from 'styled-components';
+
+import { TitleMedium } from '../../../typography';
+import { useNavigationHeader } from '../NavigationHeaderContext';
+
+export interface NavigationHeaderTitleProps {
+ children: React.ReactNode;
+}
+
+export const StyledText = styled(TitleMedium)<{ $visible?: boolean }>(({ $visible = true }) => ({
+ opacity: $visible ? 1 : 0,
+ transition: 'opacity 250ms ease-in-out',
+}));
+
+export const NavigationHeaderTitle = ({ children }: NavigationHeaderTitleProps) => {
+ const { titleVisible } = useNavigationHeader();
+ return (
+ <StyledText tag="h1" $visible={titleVisible}>
+ {children}
+ </StyledText>
+ );
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/index.ts
new file mode 100644
index 0000000000..1869233158
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/components/index.ts
@@ -0,0 +1,3 @@
+export * from './NavigationHeaderButtonGroup';
+export * from './NavigationHeaderTitle';
+export * from './NavigationHeaderIconButton';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/index.ts
new file mode 100644
index 0000000000..e5022e8bf9
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/organisms/navigation-header/index.ts
@@ -0,0 +1 @@
+export * from './NavigationHeader';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/typography/Text.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/typography/Text.tsx
index 93834cb4a8..15a964065d 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/typography/Text.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/typography/Text.tsx
@@ -1,5 +1,5 @@
-import { forwardRef } from 'react';
-import styled from 'styled-components';
+import { createElement, forwardRef } from 'react';
+import styled, { WebTarget } from 'styled-components';
import { Colors, Typography, typography, TypographyProperties } from '../../foundations';
import { TransientProps } from '../../types';
@@ -7,11 +7,15 @@ import { TransientProps } from '../../types';
export type TextProps = React.PropsWithChildren<{
variant?: Typography;
color?: Colors;
- as?: React.ElementType;
+ tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span';
+ as?: WebTarget;
style?: React.CSSProperties;
}>;
-const StyledText = styled.span<TransientProps<TypographyProperties>>((props) => ({
+const StyledText = styled(
+ ({ tag = 'span', ...props }: { tag: TextProps['tag'] } & TransientProps<TypographyProperties>) =>
+ createElement(tag, props),
+)((props) => ({
color: 'var(--color)',
fontFamily: props.$fontFamily,
fontWeight: props.$fontWeight,
@@ -20,11 +24,22 @@ const StyledText = styled.span<TransientProps<TypographyProperties>>((props) =>
}));
export const Text = forwardRef(
- ({ variant = 'bodySmall', color = Colors.white, children, style, ...props }: TextProps, ref) => {
+ (
+ {
+ tag = 'span',
+ variant = 'bodySmall',
+ color = Colors.white,
+ children,
+ style,
+ ...props
+ }: TextProps,
+ ref,
+ ) => {
const { fontFamily, fontSize, fontWeight, lineHeight } = typography[variant];
return (
<StyledText
ref={ref}
+ tag={tag}
style={
{
'--color': color,