diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx index 1cc666910ef4..115196a49d56 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() { - - - - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('account-view', 'Account') - } - - - + diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx index bf1a2025df0e..83142d29d7a3 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() { - - - - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('navigation-bar', 'API access') - } - - - - + + + diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx index 264b389f8407..a3ebac27bee2 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() { - - - {strings.daita} - - + diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx index fee86b00cc5b..6e3fe6540c0c 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() { - - - Developer tools - - + diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx index c591c816f48e..cf1c413f6b44 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 ( - + + + + diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx index 79c7de686158..6dec1e8eb612 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() { - - - {title} - - + diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx index 2ab8f810836d..6d45eda22922 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() { - - - {title} - - + diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ErrorView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ErrorView.tsx index 08ed7b09622f..647dc78b4ab8 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 ( -
{!props.settingsUnavailable && }
+ + {!props.settingsUnavailable && } + diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx index 2433a29644d8..df154e628bae 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 ; + return ( + + + + + ); } 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 a98d3e441c3d..744632c6ed5e 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 ( - + + + + {isNewAccount ? : } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorViewStyles.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorViewStyles.tsx index f41178a62760..5e6275157024 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 940563fd87c3..790215cab401 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() { - - - - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('filter-nav', 'Filter') - } - - - + ((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 ( - - {props.children} - {props.showAccountInfo && } - - ); -} - -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) { - return ( - - - - </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 7f77f115e74d..8243e5b3bebc 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 62babaac38fe..ff7c8c794372 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 0d234bd66b64..08e11b7628b2 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 e7e16b395a2b..d885dd8533b7 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 680393a6c776..000000000000 --- 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 eb4472c9005d..000000000000 --- 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 000000000000..946a66c05f3b --- /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 000000000000..81583b1c4e8c --- /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 37e72b13e736..d1288b30a1df 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 36ddc09c4556..e4213d871f1a 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 84f572fc5ac6..91ee78429a7a 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 f2c4c2760ac5..1eb725353339 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 8b7169ab06f2..b2ebe680d821 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 c6cb325b0cd2..ed2dd8469532 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 4c59bde50e65..b23fe817fcd4 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 cab859b8c97a..849b420cd122 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 554f2a2a1140..98438116c9f8 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 2785883ecf03..34f49a1c30e0 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 42c5a61373d2..23a5d68b3416 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 7179daca77a7..c6e6ff43f666 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 38a0f71bbde5..13c29a7ea73b 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 5754ebce72c9..7a21a18f6b36 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 5a87ffdb8be4..450ced9d31c9 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 000000000000..cae1bf433667 --- /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 000000000000..f66ba6d17fc5 --- /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 000000000000..d13e48645572 --- /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 000000000000..d738f502a3d1 --- /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 000000000000..2ad145e28354 --- /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 000000000000..a4b73cc7c3d9 --- /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 000000000000..80f1f794f400 --- /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 000000000000..686305180a87 --- /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 000000000000..882308e45491 --- /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 000000000000..6a81375ace19 --- /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 000000000000..5bf59572e338 --- /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 b1e20a0c4104..4fc8e159257d 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 c1b9ac80d1a9..ccca7cdc0b52 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 000000000000..612a0df28fb6 --- /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 9327094a5186..06a4ec8e6952 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 32c516d0a041..7779d6fb2b77 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 e4d105d63586..552f86d9fccc 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 41671c46a9bd..dedf6b72bb9f 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 4a93908e6524..19a499a34d21 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 000000000000..95c029cdb937 --- /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 000000000000..edc0c5aec355 --- /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 54e890858128..8a398fe7a2b5 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 0721fbf54aa6..858ecee82b99 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 d6f1dcc242f2..606f757fdc03 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 000000000000..5b82330cdf9d --- /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 000000000000..b3cf54c9d200 --- /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 000000000000..907f48cb0038 --- /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 000000000000..4d837fe571ab --- /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 000000000000..3d8a1927af28 --- /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 000000000000..9b73e18f851e --- /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 000000000000..d0111b50bdff --- /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 000000000000..b6fc3175542c --- /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 000000000000..531e97eafdac --- /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 000000000000..31164ffdea77 --- /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 000000000000..1869233158b4 --- /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 000000000000..e5022e8bf920 --- /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 93834cb4a8da..15a964065d02 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,