summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2025-04-30 11:06:47 +0200
committerMarkus Pettersson <markus.pettersson@mullvad.net>2025-04-30 11:06:47 +0200
commit9c60042e85b37143cd9e0742f591f0a01c6bc165 (patch)
tree853ddf94cee217e138ec61d98c56e461ce0c1eaf
parent532e6ff6d2902071abf92e3dbdc81dd8e1a1a862 (diff)
parenteeda8e879a9b9115ac65fa2e4c408ba4e031dbe1 (diff)
downloadmullvadvpn-9c60042e85b37143cd9e0742f591f0a01c6bc165.tar.xz
mullvadvpn-9c60042e85b37143cd9e0742f591f0a01c6bc165.zip
Merge branch 'replace-remaining-buttons-with-new-button-component-des-1794'
-rw-r--r--desktop/packages/mullvad-vpn/locales/messages.pot26
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx48
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx19
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/AppButton.tsx204
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ButtonGroup.tsx46
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/CustomDnsSettings.tsx20
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx26
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx25
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/DeviceInfoButton.tsx9
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx16
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx20
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx16
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx62
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorView.tsx76
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Filter.tsx10
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Launch.tsx36
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx5
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Modal.tsx16
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/MultiButton.tsx6
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx55
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx49
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ProblemReport.tsx134
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ProxyForm.tsx51
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/RedeemVoucher.tsx55
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SettingsImport.tsx49
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SmallButton.tsx128
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx64
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx10
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx32
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx20
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/main-view/SelectLocationButton.tsx2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/select-location/CustomListDialogs.tsx32
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx20
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocationStyles.tsx6
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/button/Button.tsx125
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonContext.tsx4
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonIcon.tsx18
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonText.tsx8
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-column/FlexColumn.tsx5
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-column/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-row/FlexRow.tsx5
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-row/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/hooks/use-exclusive-task.tsx22
43 files changed, 707 insertions, 875 deletions
diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot
index e9e1f7d813..058e342945 100644
--- a/desktop/packages/mullvad-vpn/locales/messages.pot
+++ b/desktop/packages/mullvad-vpn/locales/messages.pot
@@ -165,6 +165,7 @@ msgstr ""
msgid "Filter"
msgstr ""
+#. Button label for system settings.
msgid "Go to System Settings"
msgstr ""
@@ -365,6 +366,7 @@ msgctxt "accessibility"
msgid "New version installed, click here to see the changelog"
msgstr ""
+#. Accessibility label for the button that opens the browser to buy credit.
msgctxt "accessibility"
msgid "Opens externally"
msgstr ""
@@ -412,6 +414,7 @@ msgctxt "account-view"
msgid "Currently unavailable"
msgstr ""
+#. Button label for logging out.
msgctxt "account-view"
msgid "Log out"
msgstr ""
@@ -670,6 +673,7 @@ msgctxt "connect-view"
msgid "Connection details"
msgstr ""
+#. Button label for disconnecting from the VPN.
msgctxt "connect-view"
msgid "Disconnect"
msgstr ""
@@ -690,6 +694,7 @@ msgctxt "connect-view"
msgid "Here’s your account number. Save it!"
msgstr ""
+#. Button label for opening privacy information link.
msgctxt "connect-view"
msgid "Learn about privacy"
msgstr ""
@@ -698,6 +703,7 @@ msgctxt "connect-view"
msgid "Out of time"
msgstr ""
+#. Button label for navigating to the voucher redemption view.
msgctxt "connect-view"
msgid "Redeem voucher"
msgstr ""
@@ -706,6 +712,7 @@ msgctxt "connect-view"
msgid "Remember, turning it off will allow network traffic while the VPN is disconnected until you turn it back on under Advanced settings."
msgstr ""
+#. Button label for starting the app.
msgctxt "connect-view"
msgid "Start using the app"
msgstr ""
@@ -818,6 +825,7 @@ msgctxt "device-management"
msgid "Failed to remove device"
msgstr ""
+#. Button label for navigating to login.
msgctxt "device-management"
msgid "Go to login"
msgstr ""
@@ -854,7 +862,7 @@ msgctxt "device-management"
msgid "Too many devices"
msgstr ""
-#. Confirmation button when logging out other device.
+#. Button label for confirming logout of another device.
msgctxt "device-management"
msgid "Yes, log out device"
msgstr ""
@@ -1018,6 +1026,7 @@ msgctxt "in-app-notifications"
msgid "Read more"
msgstr ""
+#. Button label to send a problem report.
msgctxt "in-app-notifications"
msgid "Send problem report"
msgstr ""
@@ -1066,6 +1075,7 @@ msgctxt "launch-view"
msgid "Restarting your computer."
msgstr ""
+#. Button label for problem report view.
msgctxt "launch-view"
msgid "Send problem report"
msgstr ""
@@ -1430,14 +1440,17 @@ msgctxt "openvpn-settings-view"
msgid "Transport protocol"
msgstr ""
+#. Cancel button label for voucher redemption.
msgctxt "redeem-voucher-alert"
msgid "Cancel"
msgstr ""
+#. Input field label for voucher code.
msgctxt "redeem-voucher-alert"
msgid "Enter voucher code"
msgstr ""
+#. Button label for redeeming a voucher.
msgctxt "redeem-voucher-alert"
msgid "Redeem voucher"
msgstr ""
@@ -1446,6 +1459,7 @@ msgctxt "redeem-voucher-view"
msgid "An error occurred."
msgstr ""
+#. Button label for voucher redemption.
msgctxt "redeem-voucher-view"
msgid "Redeem"
msgstr ""
@@ -1702,6 +1716,7 @@ msgctxt "split-tunneling-view"
msgid "Excluded apps"
msgstr ""
+#. Button label for browsing applications with split tunneling.
msgctxt "split-tunneling-view"
msgid "Find another app"
msgstr ""
@@ -1710,6 +1725,7 @@ msgctxt "split-tunneling-view"
msgid "If it’s already running, close %(applicationName)s before launching it from here. Otherwise it might not be excluded from the VPN tunnel."
msgstr ""
+#. Button label for launching an application with split tunneling.
msgctxt "split-tunneling-view"
msgid "Launch"
msgstr ""
@@ -1748,10 +1764,12 @@ msgctxt "support-view"
msgid "Beta program"
msgstr ""
+#. Button label for continuing problem report submission with an outdated app version.
msgctxt "support-view"
msgid "Continue anyway"
msgstr ""
+#. Button text to edit the message after a failed attempt to send the problem report.
msgctxt "support-view"
msgid "Edit message"
msgstr ""
@@ -1786,10 +1804,12 @@ msgctxt "support-view"
msgid "Report a problem"
msgstr ""
+#. Button label for sending the problem report.
msgctxt "support-view"
msgid "Send"
msgstr ""
+#. Button label for sending the problem report without an email address.
msgctxt "support-view"
msgid "Send anyway"
msgstr ""
@@ -1823,14 +1843,17 @@ msgctxt "support-view"
msgid "To help you more effectively, your app’s log file will be attached to this message. Your data will remain secure and private, as it is anonymised before being sent over an encrypted channel."
msgstr ""
+#. Button label for retrying problem report submission after a failure.
msgctxt "support-view"
msgid "Try again"
msgstr ""
+#. Button label for upgrading the app to the latest version.
msgctxt "support-view"
msgid "Upgrade app"
msgstr ""
+#. Button label for opening app logs.
msgctxt "support-view"
msgid "View app logs"
msgstr ""
@@ -2011,6 +2034,7 @@ msgctxt "vpn-settings-view"
msgid "Add a server"
msgstr ""
+#. Button label to add a private IP DNS server despite warning.
msgctxt "vpn-settings-view"
msgid "Add anyway"
msgstr ""
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx
index 5a231f1495..f932139a2b 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Account.tsx
@@ -4,8 +4,10 @@ import { formatDate, hasExpired } from '../../shared/account-expiry';
import { urls } from '../../shared/constants';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
-import { Flex, Icon } from '../lib/components';
+import { Button, Flex } from '../lib/components';
+import { FlexColumn } from '../lib/components/flex-column';
import { useHistory } from '../lib/history';
+import { useExclusiveTask } from '../lib/hooks/use-exclusive-task';
import { useEffectEvent } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
import { AppNavigationHeader } from './';
@@ -19,8 +21,6 @@ import {
AccountRowValue,
DeviceRowValue,
} from './AccountStyles';
-import * as AppButton from './AppButton';
-import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import DeviceInfoButton from './DeviceInfoButton';
import { BackAction } from './KeyboardNavigation';
import { Footer, Layout, SettingsContainer } from './Layout';
@@ -32,9 +32,9 @@ export default function Account() {
const isOffline = useSelector((state) => state.connection.isBlocked);
const { updateAccountData, openUrlWithAuth, logout } = useAppContext();
- const onBuyMore = useCallback(async () => {
+ const [buyMore] = useExclusiveTask(async () => {
await openUrlWithAuth(urls.purchase);
- }, [openUrlWithAuth]);
+ });
const onMount = useEffectEvent(() => updateAccountData());
useEffect(() => onMount(), []);
@@ -83,29 +83,27 @@ export default function Account() {
</AccountRows>
<Footer>
- <AppButton.ButtonGroup>
- <AppButton.BlockingButton disabled={isOffline} onClick={onBuyMore}>
- <AriaDescriptionGroup>
- <AriaDescribed>
- <AppButton.GreenButton>
- <AppButton.Label>{messages.gettext('Buy more credit')}</AppButton.Label>
- <AriaDescription>
- <Icon
- icon="external"
- aria-label={messages.pgettext('accessibility', 'Opens externally')}
- />
- </AriaDescription>
- </AppButton.GreenButton>
- </AriaDescribed>
- </AriaDescriptionGroup>
- </AppButton.BlockingButton>
+ <FlexColumn $gap="medium">
+ <Button
+ variant="success"
+ disabled={isOffline}
+ onClick={buyMore}
+ aria-description={messages.pgettext('accessibility', 'Opens externally')}>
+ <Button.Text>{messages.gettext('Buy more credit')}</Button.Text>
+ <Button.Icon icon="external" />
+ </Button>
<RedeemVoucherButton />
- <AppButton.RedButton onClick={doLogout}>
- {messages.pgettext('account-view', 'Log out')}
- </AppButton.RedButton>
- </AppButton.ButtonGroup>
+ <Button variant="destructive" onClick={doLogout}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for logging out.
+ messages.pgettext('account-view', 'Log out')
+ }
+ </Button.Text>
+ </Button>
+ </FlexColumn>
</Footer>
</AccountContainer>
</SettingsContainer>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx
index 58425f9f3b..b3c7aca0e0 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ApiAccessMethods.tsx
@@ -6,7 +6,7 @@ import { AccessMethodSetting } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import { useApiAccessMethodTest } from '../lib/api-access-methods';
-import { Container, Flex, Spinner } from '../lib/components';
+import { Button, Container, Flex, Spinner } from '../lib/components';
import { Colors, spacings } from '../lib/foundations';
import { useHistory } from '../lib/history';
import { generateRoutePath } from '../lib/routeHelpers';
@@ -27,7 +27,6 @@ import { Layout, SettingsContainer, SettingsContent, SettingsNavigationScrollbar
import { ModalAlert, ModalAlertType } from './Modal';
import { NavigationContainer } from './NavigationContainer';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
-import { SmallButton, SmallButtonColor } from './SmallButton';
const StyledNameLabel = styled(Cell.Label)({
display: 'block',
@@ -124,7 +123,9 @@ export default function ApiAccessMethods() {
))}
</Cell.Group>
<Container size="4" $flex={1} $justifyContent="flex-end">
- <SmallButton onClick={navigateToNew}>{messages.gettext('Add')}</SmallButton>
+ <Button width="fit" onClick={navigateToNew}>
+ <Button.Text>{messages.gettext('Add')}</Button.Text>
+ </Button>
</Container>
</Flex>
</SettingsContent>
@@ -300,12 +301,12 @@ function ApiAccessMethod(props: ApiAccessMethodProps) {
isOpen={removeConfirmationVisible}
type={ModalAlertType.warning}
gridButtons={[
- <SmallButton key="cancel" onClick={hideRemoveConfirmation}>
- {messages.gettext('Cancel')}
- </SmallButton>,
- <SmallButton key="confirm" onClick={confirmRemove} color={SmallButtonColor.red}>
- {messages.gettext('Delete')}
- </SmallButton>,
+ <Button key="cancel" onClick={hideRemoveConfirmation}>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>,
+ <Button key="confirm" onClick={confirmRemove} variant="destructive">
+ <Button.Text>{messages.gettext('Delete')}</Button.Text>
+ </Button>,
]}
close={hideRemoveConfirmation}
title={sprintf(messages.pgettext('api-access-methods-view', 'Delete %(name)s?'), {
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/AppButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/AppButton.tsx
index ceec46c62f..e69de29bb2 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/AppButton.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/AppButton.tsx
@@ -1,204 +0,0 @@
-import React, { useCallback, useContext, useMemo, useState } from 'react';
-import styled from 'styled-components';
-
-import log from '../../shared/logging';
-import { Colors } from '../lib/foundations';
-import { useMounted } from '../lib/utility-hooks';
-import {
- StyledButtonContent,
- StyledHiddenSide,
- StyledLabel,
- StyledLeft,
- StyledRight,
- StyledVisibleSide,
- transparentButton,
-} from './AppButtonStyles';
-import { measurements } from './common-styles';
-
-interface ILabelProps {
- textOffset?: number;
- children?: React.ReactNode;
-}
-
-export function Label(props: ILabelProps) {
- return <StyledLabel $textOffset={props.textOffset ?? 0}>{props.children}</StyledLabel>;
-}
-
-export interface IProps extends React.HTMLAttributes<HTMLButtonElement> {
- children?: React.ReactNode;
- className?: string;
- disabled?: boolean;
- onClick?: () => void;
- textOffset?: number;
-}
-
-type ChildrenGroups = { left: React.ReactNode[]; label: React.ReactNode; right: React.ReactNode[] };
-
-const BaseButton = React.memo(function BaseButtonT(props: IProps) {
- const { children, textOffset, ...otherProps } = props;
-
- const groupedChildren = useMemo(() => {
- return React.Children.toArray(children).reduce(
- (groups: ChildrenGroups, child) => {
- if (groups.label === undefined && typeof child === 'string') {
- return { ...groups, label: <Label textOffset={textOffset}>{child}</Label> };
- } else if (React.isValidElement(child) && child.type === Label) {
- return {
- ...groups,
- label: React.cloneElement(child as React.ReactElement<ILabelProps>, { textOffset }),
- };
- } else if (groups.label === undefined) {
- return { ...groups, left: [...groups.left, child] };
- } else {
- return { ...groups, right: [...groups.right, child] };
- }
- },
- { left: [], label: undefined, right: [] },
- );
- }, [children, textOffset]);
-
- return (
- <StyledSimpleButton {...otherProps}>
- <StyledButtonContent>
- <StyledLeft>
- <StyledVisibleSide>{groupedChildren.left}</StyledVisibleSide>
- <StyledHiddenSide>{groupedChildren.right}</StyledHiddenSide>
- </StyledLeft>
-
- {groupedChildren.label ?? <Label />}
-
- <StyledRight>
- <StyledVisibleSide>{groupedChildren.right}</StyledVisibleSide>
- <StyledHiddenSide>{groupedChildren.left}</StyledHiddenSide>
- </StyledRight>
- </StyledButtonContent>
- </StyledSimpleButton>
- );
-});
-
-function SimpleButtonT(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
- const blockingContext = useContext(BlockingContext);
-
- return (
- <button
- {...props}
- disabled={props.disabled || blockingContext.disabled}
- onClick={blockingContext.onClick ?? props.onClick}>
- {props.children}
- </button>
- );
-}
-
-export const SimpleButton = React.memo(SimpleButtonT);
-
-const StyledSimpleButton = styled(SimpleButton)({
- display: 'flex',
- cursor: 'default',
- borderRadius: 4,
- border: 'none',
- padding: 0,
- '&&:disabled': {
- opacity: 0.5,
- },
-});
-
-interface IBlockingContext {
- disabled?: boolean;
- onClick?: () => Promise<void>;
-}
-
-const BlockingContext = React.createContext<IBlockingContext>({});
-
-interface IBlockingProps {
- children?: React.ReactNode;
- onClick: () => Promise<void>;
- disabled?: boolean;
-}
-
-export function BlockingButton(props: IBlockingProps) {
- const { onClick: propsOnClick } = props;
-
- const isMounted = useMounted();
- const [isBlocked, setIsBlocked] = useState(false);
-
- const onClick = useCallback(async () => {
- setIsBlocked(true);
- try {
- await propsOnClick();
- } catch (error) {
- log.error(`onClick() failed - ${error}`);
- }
-
- if (isMounted()) {
- setIsBlocked(false);
- }
- }, [isMounted, propsOnClick]);
-
- const contextValue = useMemo(
- () => ({
- disabled: isBlocked || props.disabled,
- onClick,
- }),
- [isBlocked, props.disabled, onClick],
- );
-
- return <BlockingContext.Provider value={contextValue}>{props.children}</BlockingContext.Provider>;
-}
-
-export const RedButton = styled(BaseButton)({
- backgroundColor: Colors.red,
- '&&:not(:disabled):hover': {
- backgroundColor: Colors.red95,
- },
-});
-
-export const GreenButton = styled(BaseButton)({
- backgroundColor: Colors.green,
- '&&:not(:disabled):hover': {
- backgroundColor: Colors.green90,
- },
-});
-
-export const BlueButton = styled(BaseButton)({
- backgroundColor: Colors.blue80,
- '&&:not(:disabled):hover': {
- backgroundColor: Colors.blue60,
- },
-});
-
-export const TransparentButton = styled(BaseButton)(transparentButton, {
- backgroundColor: Colors.white20,
- '&&:not(:disabled):hover': {
- backgroundColor: Colors.white40,
- },
-});
-
-export const RedTransparentButton = styled(BaseButton)(transparentButton, {
- backgroundColor: Colors.red60,
- '&&:not(:disabled):hover': {
- backgroundColor: Colors.red80,
- },
-});
-
-const StyledButtonWrapper = styled.div({
- display: 'flex',
- flexDirection: 'column',
- flex: 0,
- '&&:not(:last-child)': {
- marginBottom: measurements.buttonVerticalMargin,
- },
-});
-
-interface IButtonGroupProps {
- children: React.ReactNode | React.ReactNode[];
-}
-
-export function ButtonGroup(props: IButtonGroupProps) {
- return (
- <>
- {React.Children.map(props.children, (button, index) => (
- <StyledButtonWrapper key={index}>{button}</StyledButtonWrapper>
- ))}
- </>
- );
-}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ButtonGroup.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ButtonGroup.tsx
new file mode 100644
index 0000000000..357d93ec37
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ButtonGroup.tsx
@@ -0,0 +1,46 @@
+import styled, { css } from 'styled-components';
+
+import { Flex, FlexProps } from '../lib/components';
+import { StyledButton } from '../lib/components/button';
+
+export type WrapButtonGroupProps = FlexProps;
+
+const StyledFlex = styled(Flex)`
+ ${({ $gap: gapProp }) => {
+ const $gap = gapProp ?? '0px';
+ return css`
+ && > ${StyledButton} {
+ flex: 1 0 auto;
+ max-width: 100%;
+ }
+ &&:has(> :nth-child(2)) {
+ & > ${StyledButton} {
+ min-width: calc((100% - ${$gap}) / 2);
+ }
+ }
+ &&:has(> :nth-child(3)) {
+ & > ${StyledButton} {
+ min-width: calc((100% - ${$gap} * 2) / 3);
+ }
+ }
+ &&:has(> :nth-child(4)) {
+ & > ${StyledButton} {
+ min-width: calc((100% - ${$gap} * 3) / 4);
+ }
+ }
+ &&:has(> :nth-child(5)) {
+ & > ${StyledButton} {
+ min-width: calc((100% - ${$gap} * 4) / 5);
+ }
+ }
+ `;
+ }}
+`;
+
+export function ButtonGroup({ $gap, children, ...props }: WrapButtonGroupProps) {
+ return (
+ <StyledFlex $gap={$gap} $flexWrap="wrap" {...props}>
+ {children}
+ </StyledFlex>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/CustomDnsSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/CustomDnsSettings.tsx
index 1be8d0be2e..cb01154020 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/CustomDnsSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/CustomDnsSettings.tsx
@@ -2,13 +2,12 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
-import { IconButton } from '../lib/components';
+import { Button, IconButton } from '../lib/components';
import { formatHtml } from '../lib/html-formatter';
import { IpAddress } from '../lib/ip';
import { useBoolean, useMounted, useStyledRef } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
import Accordion from './Accordion';
-import * as AppButton from './AppButton';
import {
AriaDescribed,
AriaDescription,
@@ -386,12 +385,17 @@ function ConfirmationDialog(props: IConfirmationDialogProps) {
isOpen={props.isOpen}
type={ModalAlertType.caution}
buttons={[
- <AppButton.RedButton key="confirm" onClick={props.confirm}>
- {messages.pgettext('vpn-settings-view', 'Add anyway')}
- </AppButton.RedButton>,
- <AppButton.BlueButton key="back" onClick={props.abort}>
- {messages.gettext('Back')}
- </AppButton.BlueButton>,
+ <Button variant="destructive" key="confirm" onClick={props.confirm}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label to add a private IP DNS server despite warning.
+ messages.pgettext('vpn-settings-view', 'Add anyway')
+ }
+ </Button.Text>
+ </Button>,
+ <Button key="back" onClick={props.abort}>
+ <Button.Text>{messages.gettext('Back')}</Button.Text>
+ </Button>,
]}
close={props.abort}
message={message}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx
index 477f9f3f45..4a622527ae 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/DaitaSettings.tsx
@@ -5,7 +5,7 @@ import styled from 'styled-components';
import { strings } from '../../shared/constants';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
-import { Flex } from '../lib/components';
+import { Button, Flex } from '../lib/components';
import { spacings } from '../lib/foundations';
import { useHistory } from '../lib/history';
import { useBoolean } from '../lib/utility-hooks';
@@ -21,7 +21,6 @@ import { NavigationContainer } from './NavigationContainer';
import { NavigationScrollbars } from './NavigationScrollbars';
import PageSlider from './PageSlider';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
-import { SmallButton, SmallButtonColor } from './SmallButton';
const StyledHeaderSubTitle = styled(HeaderSubTitle)({
display: 'inline-block',
@@ -224,18 +223,17 @@ function DaitaToggle() {
isOpen={confirmationDialogVisible}
type={ModalAlertType.caution}
gridButtons={[
- <SmallButton
- key="confirm"
- onClick={confirmEnableDirectOnly}
- color={SmallButtonColor.blue}>
- {
- // TRANSLATORS: A toggle that refers to the setting "Direct only".
- messages.gettext('Enable direct only')
- }
- </SmallButton>,
- <SmallButton key="cancel" onClick={hideConfirmationDialog} color={SmallButtonColor.blue}>
- {messages.pgettext('wireguard-settings-view', 'Cancel')}
- </SmallButton>,
+ <Button key="cancel" onClick={hideConfirmationDialog}>
+ <Button.Text>{messages.pgettext('wireguard-settings-view', 'Cancel')}</Button.Text>
+ </Button>,
+ <Button key="confirm" onClick={confirmEnableDirectOnly}>
+ <Button.Text>
+ {
+ // TRANSLATORS: A toggle that refers to the setting "Direct only".
+ messages.gettext('Enable direct only')
+ }
+ </Button.Text>
+ </Button>,
]}
close={hideConfirmationDialog}>
<ModalMessage>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx
index e9bb8a1204..4a3c1e80b5 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Debug.tsx
@@ -1,11 +1,12 @@
import { useCallback } from 'react';
import styled from 'styled-components';
+import { Button } from '../lib/components';
+import { FlexColumn } from '../lib/components/flex-column';
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';
@@ -41,11 +42,11 @@ export default function Debug() {
<StyledContent>
<StyledButtonGroup>
- <AppButton.ButtonGroup>
+ <FlexColumn $gap="medium">
<ThrowErrorButton />
<UnhandledRejectionButton />
<ErrorDuringRender />
- </AppButton.ButtonGroup>
+ </FlexColumn>
</StyledButtonGroup>
</StyledContent>
</NavigationScrollbars>
@@ -61,7 +62,11 @@ function ThrowErrorButton() {
throw new Error('This is a test error');
}, []);
- return <AppButton.RedButton onClick={handleClick}>Throw error</AppButton.RedButton>;
+ return (
+ <Button variant="destructive" onClick={handleClick}>
+ <Button.Text>Throw error</Button.Text>
+ </Button>
+ );
}
function UnhandledRejectionButton() {
@@ -69,7 +74,11 @@ function UnhandledRejectionButton() {
return new Promise((_resolve, reject) => setTimeout(reject, 100));
}, []);
- return <AppButton.RedButton onClick={handleClick}>Unhandled rejection</AppButton.RedButton>;
+ return (
+ <Button variant="destructive" onClick={handleClick}>
+ <Button.Text>Unhandled rejection</Button.Text>
+ </Button>
+ );
}
function ErrorDuringRender() {
@@ -79,5 +88,9 @@ function ErrorDuringRender() {
throw new Error('This is a test error during render');
}
- return <AppButton.RedButton onClick={setError}>Error next render</AppButton.RedButton>;
+ return (
+ <Button variant="destructive" onClick={setError}>
+ <Button.Text>Error next render</Button.Text>
+ </Button>
+ );
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/DeviceInfoButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/DeviceInfoButton.tsx
index 6100fe292c..7d1274c281 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/DeviceInfoButton.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/DeviceInfoButton.tsx
@@ -1,7 +1,6 @@
import { messages } from '../../shared/gettext';
-import { IconButton } from '../lib/components';
+import { Button, IconButton } from '../lib/components';
import { useBoolean } from '../lib/utility-hooks';
-import * as AppButton from './AppButton';
import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
export default function DeviceInfoButton() {
@@ -19,9 +18,9 @@ export default function DeviceInfoButton() {
isOpen={deviceHelpVisible}
type={ModalAlertType.info}
buttons={[
- <AppButton.BlueButton key="back" onClick={hideDeviceHelp}>
- {messages.gettext('Got it!')}
- </AppButton.BlueButton>,
+ <Button key="back" onClick={hideDeviceHelp}>
+ <Button.Text>{messages.gettext('Got it!')}</Button.Text>
+ </Button>,
]}
close={hideDeviceHelp}>
<ModalMessage>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx
index 93d55f1a39..642617db3d 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/DeviceRevokedView.tsx
@@ -2,12 +2,11 @@ import styled from 'styled-components';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
-import { Flex } from '../lib/components';
+import { Button, Flex } from '../lib/components';
import { Colors } from '../lib/foundations';
import { IconBadge } from '../lib/icon-badge';
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 { Container, Footer, Layout } from './Layout';
@@ -44,8 +43,6 @@ export function DeviceRevokedView() {
const { leaveRevokedDevice } = useAppContext();
const tunnelState = useSelector((state) => state.connection.status);
- const Button = tunnelState.state === 'disconnected' ? AppButton.BlueButton : AppButton.RedButton;
-
return (
<Layout>
<AppMainHeader variant="basedOnConnectionStatus" size="basedOnLoginStatus">
@@ -77,8 +74,15 @@ export function DeviceRevokedView() {
</StyledBody>
<Footer>
- <Button onClick={leaveRevokedDevice}>
- {messages.pgettext('device-management', 'Go to login')}
+ <Button
+ variant={tunnelState.state === 'disconnected' ? 'primary' : 'destructive'}
+ onClick={leaveRevokedDevice}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for navigating to login.
+ messages.pgettext('device-management', 'Go to login')
+ }
+ </Button.Text>
</Button>
</Footer>
</StyledContainer>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx
index 6dec1e8eb6..f3c55965d8 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx
@@ -11,6 +11,7 @@ import { messages } from '../../shared/gettext';
import { useScheduler } from '../../shared/scheduler';
import { useAppContext } from '../context';
import { useApiAccessMethodTest } from '../lib/api-access-methods';
+import { Button } from '../lib/components';
import { useHistory } from '../lib/history';
import { useLastDefinedValue } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
@@ -22,7 +23,6 @@ import { ModalAlert, ModalAlertType } from './Modal';
import { NavigationContainer } from './NavigationContainer';
import { NamedProxyForm } from './ProxyForm';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
-import { SmallButton } from './SmallButton';
export function EditApiAccessMethod() {
return (
@@ -216,19 +216,19 @@ function getTestingDialogSubTitle(type: ModalAlertType, newMethod: boolean, name
function getTestingDialogButtons(type: ModalAlertType, save: () => void, cancel: () => void) {
const saveButton = (
- <SmallButton key="confirm" onClick={save}>
- {messages.gettext('Save')}
- </SmallButton>
+ <Button key="confirm" onClick={save}>
+ <Button.Text>{messages.gettext('Save')}</Button.Text>
+ </Button>
);
const cancelButton = (
- <SmallButton key="cancel" onClick={cancel}>
- {messages.gettext('Cancel')}
- </SmallButton>
+ <Button key="cancel" onClick={cancel}>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>
);
const disabledCancelButton = (
- <SmallButton key="cancel" onClick={cancel} disabled>
- {messages.gettext('Cancel')}
- </SmallButton>
+ <Button key="cancel" onClick={cancel} disabled>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>
);
switch (type) {
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx
index 6d45eda229..560b6ccc3f 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/EditCustomBridge.tsx
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { CustomProxy } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
+import { Button } from '../lib/components';
import { useBridgeSettingsUpdater } from '../lib/constraint-updater';
import { useHistory } from '../lib/history';
import { useBoolean } from '../lib/utility-hooks';
@@ -14,7 +15,6 @@ import { ModalAlert, ModalAlertType } from './Modal';
import { NavigationContainer } from './NavigationContainer';
import { ProxyForm } from './ProxyForm';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-import { SmallButton, SmallButtonColor } from './SmallButton';
export function EditCustomBridge() {
return (
@@ -86,16 +86,16 @@ function CustomBridgeForm() {
isOpen={deleteDialogVisible}
type={ModalAlertType.warning}
gridButtons={[
- <SmallButton key="cancel" onClick={hideDeleteDialog}>
- {messages.gettext('Cancel')}
- </SmallButton>,
- <SmallButton
+ <Button key="cancel" onClick={hideDeleteDialog}>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>,
+ <Button
key="delete"
- color={SmallButtonColor.red}
+ variant="destructive"
onClick={onDelete}
data-testid="delete-confirm">
- {messages.gettext('Delete')}
- </SmallButton>,
+ <Button.Text>{messages.gettext('Delete')}</Button.Text>
+ </Button>,
]}
close={hideDeleteDialog}
title={messages.pgettext('custom-bridge', 'Delete custom bridge?')}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx
index aae0e599b1..97eea51960 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountAddTime.tsx
@@ -9,7 +9,8 @@ import { formatRelativeDate } from '../../shared/date-helper';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
-import { Flex, Icon } from '../lib/components';
+import { Button, Flex } from '../lib/components';
+import { FlexColumn } from '../lib/components/flex-column';
import { Colors } from '../lib/foundations';
import { TransitionType, useHistory } from '../lib/history';
import { IconBadge } from '../lib/icon-badge';
@@ -18,8 +19,6 @@ 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 { Container, Footer, Layout } from './Layout';
@@ -99,12 +98,12 @@ export function VoucherInput() {
</StyledBody>
<Footer>
- <AppButton.ButtonGroup>
+ <FlexColumn $gap="medium">
<RedeemVoucherSubmitButton />
- <AppButton.BlueButton onClick={navigateBack}>
- {messages.gettext('Cancel')}
- </AppButton.BlueButton>
- </AppButton.ButtonGroup>
+ <Button onClick={navigateBack}>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>
+ </FlexColumn>
</Footer>
</RedeemVoucherContainer>
</StyledContainer>
@@ -188,9 +187,9 @@ export function TimeAdded(props: ITimeAddedProps) {
</StyledBody>
<Footer>
- <AppButton.BlueButton onClick={navigateToSetupFinished}>
- {messages.gettext('Next')}
- </AppButton.BlueButton>
+ <Button onClick={navigateToSetupFinished}>
+ <Button.Text>{messages.gettext('Next')}</Button.Text>
+ </Button>
</Footer>
</StyledContainer>
</StyledCustomScrollbars>
@@ -226,26 +225,27 @@ export function SetupFinished() {
</StyledBody>
<Footer>
- <AppButton.ButtonGroup>
- <AriaDescriptionGroup>
- <AriaDescribed>
- <AppButton.BlueButton onClick={openPrivacyLink}>
- <AppButton.Label>
- {messages.pgettext('connect-view', 'Learn about privacy')}
- </AppButton.Label>
- <AriaDescription>
- <Icon
- icon="external"
- aria-label={messages.pgettext('accessibility', 'Opens externally')}
- />
- </AriaDescription>
- </AppButton.BlueButton>
- </AriaDescribed>
- </AriaDescriptionGroup>
- <AppButton.GreenButton onClick={finish}>
- {messages.pgettext('connect-view', 'Start using the app')}
- </AppButton.GreenButton>
- </AppButton.ButtonGroup>
+ <FlexColumn $gap="medium">
+ <Button
+ onClick={openPrivacyLink}
+ aria-description={messages.pgettext('accessibility', 'Opens externally')}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for opening privacy information link.
+ messages.pgettext('connect-view', 'Learn about privacy')
+ }
+ </Button.Text>
+ <Button.Icon icon="external" />
+ </Button>
+ <Button variant="success" onClick={finish}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for starting the app.
+ messages.pgettext('connect-view', 'Start using the app')
+ }
+ </Button.Text>
+ </Button>
+ </FlexColumn>
</Footer>
</StyledContainer>
</StyledCustomScrollbars>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorView.tsx
index 0f80d890de..7b96834c1c 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorView.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ExpiredAccountErrorView.tsx
@@ -6,14 +6,14 @@ import { messages } from '../../shared/gettext';
import log from '../../shared/logging';
import { capitalizeEveryWord } from '../../shared/string-helpers';
import { useAppContext } from '../context';
-import { Flex, Icon } from '../lib/components';
+import { Button, Flex } from '../lib/components';
+import { FlexColumn } from '../lib/components/flex-column';
import { useHistory } from '../lib/history';
+import { useExclusiveTask } from '../lib/hooks/use-exclusive-task';
import { IconBadge } from '../lib/icon-badge';
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';
import DeviceInfoButton from './DeviceInfoButton';
import {
@@ -52,14 +52,14 @@ function ExpiredAccountErrorViewComponent() {
const { recoveryAction } = useRecoveryAction();
const isNewAccount = useIsNewAccount();
- const onDisconnect = useCallback(async () => {
+ const [disconnect, disconnecting] = useExclusiveTask(async () => {
try {
await disconnectTunnel();
} catch (e) {
const error = e as Error;
log.error(`Failed to disconnect the tunnel: ${error.message}`);
}
- }, [disconnectTunnel]);
+ });
const navigateToRedeemVoucher = useCallback(() => {
push(RoutePath.redeemVoucher);
@@ -78,21 +78,29 @@ function ExpiredAccountErrorViewComponent() {
<StyledBody>{isNewAccount ? <WelcomeView /> : <Content />}</StyledBody>
<Footer>
- <AppButton.ButtonGroup>
+ <FlexColumn $gap="medium">
{recoveryAction === RecoveryAction.disconnect && (
- <AppButton.BlockingButton onClick={onDisconnect}>
- <AppButton.RedButton>
- {messages.pgettext('connect-view', 'Disconnect')}
- </AppButton.RedButton>
- </AppButton.BlockingButton>
+ <Button variant="destructive" disabled={disconnecting} onClick={disconnect}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for disconnecting from the VPN.
+ messages.pgettext('connect-view', 'Disconnect')
+ }
+ </Button.Text>
+ </Button>
)}
<ExternalPaymentButton />
- <AppButton.GreenButton onClick={navigateToRedeemVoucher}>
- {messages.pgettext('connect-view', 'Redeem voucher')}
- </AppButton.GreenButton>
- </AppButton.ButtonGroup>
+ <Button variant="success" onClick={navigateToRedeemVoucher}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for navigating to the voucher redemption view.
+ messages.pgettext('connect-view', 'Redeem voucher')
+ }
+ </Button.Text>
+ </Button>
+ </FlexColumn>
</Footer>
<BlockWhenDisconnectedAlert />
@@ -184,32 +192,26 @@ function ExternalPaymentButton() {
? messages.gettext('Buy credit')
: messages.gettext('Buy more credit');
- const onOpenExternalPayment = useCallback(async () => {
+ const [openExternalPayment, openingExternalPayment] = useExclusiveTask(async () => {
if (recoveryAction === RecoveryAction.disableBlockedWhenDisconnected) {
setShowBlockWhenDisconnectedAlert(true);
} else {
await openUrlWithAuth(urls.purchase);
}
- }, [openUrlWithAuth, recoveryAction, setShowBlockWhenDisconnectedAlert]);
+ });
return (
- <AppButton.BlockingButton
- disabled={recoveryAction === RecoveryAction.disconnect}
- onClick={onOpenExternalPayment}>
- <AriaDescriptionGroup>
- <AriaDescribed>
- <AppButton.GreenButton>
- <AppButton.Label>{buttonText}</AppButton.Label>
- <AriaDescription>
- <Icon
- icon="external"
- aria-label={messages.pgettext('accessibility', 'Opens externally')}
- />
- </AriaDescription>
- </AppButton.GreenButton>
- </AriaDescribed>
- </AriaDescriptionGroup>
- </AppButton.BlockingButton>
+ <Button
+ variant="success"
+ disabled={openingExternalPayment || recoveryAction === RecoveryAction.disconnect}
+ onClick={openExternalPayment}
+ aria-description={
+ // TRANSLATORS: Accessibility label for the button that opens the browser to buy credit.
+ messages.pgettext('accessibility', 'Opens externally')
+ }>
+ <Button.Text>{buttonText}</Button.Text>
+ <Button.Icon icon="external" />
+ </Button>
);
}
@@ -240,9 +242,9 @@ function BlockWhenDisconnectedAlert() {
isOpen={showBlockWhenDisconnectedAlert}
type={ModalAlertType.caution}
buttons={[
- <AppButton.BlueButton key="cancel" onClick={onCloseBlockWhenDisconnectedInstructions}>
- {messages.gettext('Close')}
- </AppButton.BlueButton>,
+ <Button key="cancel" onClick={onCloseBlockWhenDisconnectedInstructions}>
+ <Button.Text>{messages.gettext('Close')}</Button.Text>
+ </Button>,
]}
close={onCloseBlockWhenDisconnectedInstructions}>
<ModalMessage>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Filter.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Filter.tsx
index aa08ade946..5e762ac62c 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Filter.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Filter.tsx
@@ -3,7 +3,7 @@ import styled from 'styled-components';
import { Ownership } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
-import { Icon } from '../lib/components';
+import { Button, Icon } from '../lib/components';
import { useRelaySettingsUpdater } from '../lib/constraint-updater';
import {
EndpointType,
@@ -18,7 +18,6 @@ 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';
import * as Cell from './cell';
import Selector from './cell/Selector';
@@ -99,11 +98,12 @@ export default function Filter() {
/>
</StyledNavigationScrollbars>
<Footer>
- <AppButton.GreenButton
+ <Button
+ variant="success"
disabled={Object.values(providers).every((provider) => !provider)}
onClick={onApply}>
- {messages.gettext('Apply')}
- </AppButton.GreenButton>
+ <Button.Text>{messages.gettext('Apply')}</Button.Text>
+ </Button>
</Footer>
</NavigationContainer>
</SettingsContainer>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Launch.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Launch.tsx
index 530e6faa59..61432e6c74 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Launch.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Launch.tsx
@@ -3,12 +3,12 @@ import styled from 'styled-components';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
+import { Button } from '../lib/components';
import { Colors } from '../lib/foundations';
import { TransitionType, useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { useBoolean } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
-import * as AppButton from './AppButton';
import { measurements, tinyText } from './common-styles';
import ErrorView from './ErrorView';
import { Footer } from './Layout';
@@ -61,9 +61,14 @@ function MacOsPermissionFooter() {
'Permission for the Mullvad VPN service has been revoked. Please go to System Settings and allow Mullvad VPN under the “Allow in the Background” setting.',
)}
</StyledFooterMessage>
- <AppButton.BlueButton onClick={openSettings}>
- {messages.gettext('Go to System Settings')}
- </AppButton.BlueButton>
+ <Button onClick={openSettings}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for system settings.
+ messages.gettext('Go to System Settings')
+ }
+ </Button.Text>
+ </Button>
</StyledFooterInner>
</StyledFooter>
);
@@ -88,9 +93,9 @@ function DefaultFooter() {
'Unable to contact the Mullvad system service, your connection might be unsecure. Please troubleshoot or send a problem report by clicking the Learn more button.',
)}
</StyledFooterMessage>
- <AppButton.BlueButton onClick={showDialog}>
- {messages.gettext('Learn more')}
- </AppButton.BlueButton>
+ <Button onClick={showDialog}>
+ <Button.Text>{messages.gettext('Learn more')}</Button.Text>
+ </Button>
</StyledFooterInner>
</StyledFooter>
<ModalAlert
@@ -98,12 +103,17 @@ function DefaultFooter() {
type={ModalAlertType.info}
close={hideDialog}
buttons={[
- <AppButton.GreenButton key="problem-report" onClick={openSendProblemReport}>
- {messages.pgettext('launch-view', 'Send problem report')}
- </AppButton.GreenButton>,
- <AppButton.BlueButton key="back" onClick={hideDialog}>
- {messages.gettext('Back')}
- </AppButton.BlueButton>,
+ <Button variant="success" key="problem-report" onClick={openSendProblemReport}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for problem report view.
+ messages.pgettext('launch-view', 'Send problem report')
+ }
+ </Button.Text>
+ </Button>,
+ <Button key="back" onClick={hideDialog}>
+ <Button.Text>{messages.gettext('Back')}</Button.Text>
+ </Button>,
]}>
<ModalMessage>
{messages.pgettext(
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx
index afb75ce500..ccd122eac0 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx
@@ -17,7 +17,6 @@ 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 { Container, Layout } from './Layout';
import {
StyledAccountDropdownContainer,
@@ -515,7 +514,9 @@ function BlockMessage() {
<StyledBlockMessageContainer>
<StyledBlockTitle>{messages.gettext('Blocking internet')}</StyledBlockTitle>
<StyledBlockMessage>{message}</StyledBlockMessage>
- <AppButton.RedButton onClick={unlock}>{buttonText}</AppButton.RedButton>
+ <Button variant="destructive" onClick={unlock}>
+ <Button.Text>{buttonText}</Button.Text>
+ </Button>
</StyledBlockMessageContainer>
);
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Modal.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Modal.tsx
index c8e3770bbd..41808d487d 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Modal.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Modal.tsx
@@ -4,14 +4,14 @@ import styled from 'styled-components';
import log from '../../shared/logging';
import { Icon, IconProps, Spinner } from '../lib/components';
+import { FlexColumn } from '../lib/components/flex-column';
import { Colors } from '../lib/foundations';
import { IconBadge } from '../lib/icon-badge';
import { useEffectEvent } from '../lib/utility-hooks';
-import * as AppButton from './AppButton';
+import { ButtonGroup } from './ButtonGroup';
import { measurements, normalText, tinyText } from './common-styles';
import CustomScrollbars from './CustomScrollbars';
import { BackAction } from './KeyboardNavigation';
-import { SmallButtonGrid } from './SmallButton';
const MODAL_CONTAINER_ID = 'modal-container';
@@ -153,10 +153,6 @@ const ModalAlertButtonGroupContainer = styled.div({
marginTop: measurements.buttonVerticalMargin,
});
-const StyledSmallButtonGrid = styled(SmallButtonGrid)({
- marginRight: '16px',
-});
-
const ModalAlertButtonContainer = styled.div({
display: 'flex',
flexDirection: 'column',
@@ -289,14 +285,16 @@ class ModalAlertImpl extends React.Component<IModalAlertImplProps, IModalAlertSt
<ModalAlertButtonGroupContainer>
{this.props.gridButtons && (
- <StyledSmallButtonGrid>{this.props.gridButtons}</StyledSmallButtonGrid>
+ <ButtonGroup $gap="small" $flexWrap="wrap-reverse" $margin={{ right: 'medium' }}>
+ {this.props.gridButtons}
+ </ButtonGroup>
)}
{this.props.buttons && (
- <AppButton.ButtonGroup>
+ <FlexColumn $gap="small">
{this.props.buttons.map((button, index) => (
<ModalAlertButtonContainer key={index}>{button}</ModalAlertButtonContainer>
))}
- </AppButton.ButtonGroup>
+ </FlexColumn>
)}
</ModalAlertButtonGroupContainer>
</StyledModalAlert>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/MultiButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/MultiButton.tsx
index c4e6293f10..e3d773888a 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/MultiButton.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/MultiButton.tsx
@@ -1,14 +1,14 @@
import React from 'react';
import styled from 'styled-components';
-import { ButtonProps } from '../lib/components';
+import { Button, ButtonProps } from '../lib/components';
const ButtonRow = styled.div({
display: 'flex',
gap: '1px',
});
-const MainButton = styled.button({
+const MainButton = styled(Button)({
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
paddingLeft: '44px',
@@ -17,7 +17,7 @@ const MainButton = styled.button({
},
});
-const SideButton = styled.button({
+const SideButton = styled(Button)({
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
'&:focus-visible': {
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx
index c27475773a..cd6d25c3a6 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationArea.tsx
@@ -16,6 +16,7 @@ import {
} from '../../shared/notifications';
import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
+import { Button } from '../lib/components';
import { TransitionType, useHistory } from '../lib/history';
import {
NewDeviceNotificationProvider,
@@ -27,7 +28,6 @@ import { useTunnelProtocol } from '../lib/relay-settings-hooks';
import { RoutePath } from '../lib/routes';
import accountActions from '../redux/account/actions';
import { IReduxState, useSelector } from '../redux/store';
-import * as AppButton from './AppButton';
import { ModalAlert, ModalAlertType, ModalMessage, ModalMessageList } from './Modal';
import {
NotificationActions,
@@ -238,43 +238,38 @@ function NotificationActionWrapper({
}
const problemReportButton = action.troubleshoot?.buttons ? (
- <AppButton.BlueButton key="problem-report" onClick={goToProblemReport}>
- {messages.pgettext('in-app-notifications', 'Send problem report')}
- </AppButton.BlueButton>
+ <Button key="problem-report" onClick={goToProblemReport}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label to send a problem report.
+ messages.pgettext('in-app-notifications', 'Send problem report')
+ }
+ </Button.Text>
+ </Button>
) : (
- <AppButton.GreenButton key="problem-report" onClick={goToProblemReport}>
- {messages.pgettext('in-app-notifications', 'Send problem report')}
- </AppButton.GreenButton>
+ <Button variant="success" key="problem-report" onClick={goToProblemReport}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label to send a problem report.
+ messages.pgettext('in-app-notifications', 'Send problem report')
+ }
+ </Button.Text>
+ </Button>
);
let buttons = [
problemReportButton,
- <AppButton.BlueButton key="back" onClick={closeTroubleshootModal}>
- {messages.gettext('Back')}
- </AppButton.BlueButton>,
+ <Button key="back" onClick={closeTroubleshootModal}>
+ <Button.Text>{messages.gettext('Back')}</Button.Text>
+ </Button>,
];
if (action.troubleshoot?.buttons) {
- const actionButtons = action.troubleshoot.buttons.map(({ variant, label, action }) => {
- if (variant === 'success')
- return (
- <AppButton.GreenButton key={label} onClick={action}>
- {label}
- </AppButton.GreenButton>
- );
- else if (variant === 'destructive')
- return (
- <AppButton.RedButton key={label} onClick={action}>
- {label}
- </AppButton.RedButton>
- );
- else
- return (
- <AppButton.BlueButton key={label} onClick={action}>
- {label}
- </AppButton.BlueButton>
- );
- });
+ const actionButtons = action.troubleshoot.buttons.map(({ variant, label, action }) => (
+ <Button key={label} variant={variant} onClick={action}>
+ <Button.Text>{label}</Button.Text>
+ </Button>
+ ));
buttons = actionButtons.concat(buttons);
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx
index 3f6da5eb09..93399b460c 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/NotificationBanner.tsx
@@ -3,10 +3,10 @@ import styled from 'styled-components';
import { messages } from '../../shared/gettext';
import { InAppNotificationIndicatorType } from '../../shared/notifications/notification';
-import { Icon, IconButton } from '../lib/components';
+import { IconButton } from '../lib/components';
import { Colors } from '../lib/foundations';
+import { useExclusiveTask } from '../lib/hooks/use-exclusive-task';
import { useEffectEvent, useLastDefinedValue, useStyledRef } from '../lib/utility-hooks';
-import * as AppButton from './AppButton';
import { tinyText } from './common-styles';
const NOTIFICATION_AREA_ID = 'notification-area';
@@ -15,45 +15,46 @@ export const NotificationTitle = styled.span(tinyText, {
color: Colors.white,
});
-export const NotificationActionButton = styled(AppButton.SimpleButton)({
- flex: 1,
- justifyContent: 'center',
- cursor: 'default',
- padding: '4px',
- background: 'transparent',
- border: 'none',
+export const NotificationSubtitleText = styled.span(tinyText, {
+ color: Colors.white60,
});
-export const NotificationActionButtonInner = styled(Icon)({
- [NotificationActionButton + ':hover &&']: {
- backgroundColor: Colors.white80,
- },
-});
+interface INotificationSubtitleProps {
+ children?: React.ReactNode;
+}
+
+export function NotificationSubtitle(props: INotificationSubtitleProps) {
+ return React.Children.count(props.children) > 0 ? <NotificationSubtitleText {...props} /> : null;
+}
interface NotificationActionProps {
onClick: () => Promise<void>;
}
export function NotificationOpenLinkAction(props: NotificationActionProps) {
+ const [onClick] = useExclusiveTask(props.onClick);
return (
- <AppButton.BlockingButton onClick={props.onClick}>
- <NotificationActionButton
- aria-describedby={NOTIFICATION_AREA_ID}
- aria-label={messages.gettext('Open URL')}>
- <NotificationActionButtonInner size="small" icon="external" color={Colors.white60} />
- </NotificationActionButton>
- </AppButton.BlockingButton>
+ <IconButton
+ size="small"
+ variant="secondary"
+ onClick={onClick}
+ aria-describedby={NOTIFICATION_AREA_ID}
+ aria-label={messages.gettext('Open URL')}>
+ <IconButton.Icon icon="external" />
+ </IconButton>
);
}
export function NotificationTroubleshootDialogAction(props: NotificationActionProps) {
return (
- <NotificationActionButton
+ <IconButton
+ size="small"
+ variant="secondary"
aria-describedby={NOTIFICATION_AREA_ID}
aria-label={messages.gettext('Troubleshoot')}
onClick={props.onClick}>
- <NotificationActionButtonInner size="small" icon="info-circle" />
- </NotificationActionButton>
+ <IconButton.Icon icon="info-circle" />
+ </IconButton>
);
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ProblemReport.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ProblemReport.tsx
index cc64f1f41f..5c808fe8dc 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ProblemReport.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ProblemReport.tsx
@@ -16,15 +16,14 @@ import { messages } from '../../shared/gettext';
import { getDownloadUrl } from '../../shared/version';
import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
-import { Flex, Icon, Spinner } from '../lib/components';
+import { Button, Flex, Spinner } from '../lib/components';
+import { FlexColumn } from '../lib/components/flex-column';
import { useHistory } from '../lib/history';
import { IconBadge } from '../lib/icon-badge';
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 { BackAction } from './KeyboardNavigation';
import { Footer, Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
@@ -180,26 +179,28 @@ function Form() {
</StyledFormMessageRow>
</StyledForm>
<Footer>
- <AriaDescriptionGroup>
- <AriaDescribed>
- <AppButton.ButtonGroup>
- <AppButton.BlueButton onClick={onViewLog} disabled={disableActions}>
- <AppButton.Label>
- {messages.pgettext('support-view', 'View app logs')}
- </AppButton.Label>
- <AriaDescription>
- <Icon
- icon="external"
- aria-label={messages.pgettext('accessibility', 'Opens externally')}
- />
- </AriaDescription>
- </AppButton.BlueButton>
- </AppButton.ButtonGroup>
- </AriaDescribed>
- </AriaDescriptionGroup>
- <AppButton.GreenButton disabled={!validate() || disableActions} onClick={onSend}>
- {messages.pgettext('support-view', 'Send')}
- </AppButton.GreenButton>
+ <FlexColumn $gap="medium">
+ <Button
+ onClick={onViewLog}
+ disabled={disableActions}
+ aria-description={messages.pgettext('accessibility', 'Opens externally')}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for opening app logs.
+ messages.pgettext('support-view', 'View app logs')
+ }
+ </Button.Text>
+ <Button.Icon icon="external" />
+ </Button>
+ <Button variant="success" disabled={!validate() || disableActions} onClick={onSend}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for sending the problem report.
+ messages.pgettext('support-view', 'Send')
+ }
+ </Button.Text>
+ </Button>
+ </FlexColumn>
</Footer>
</StyledContent>
);
@@ -270,14 +271,24 @@ function Failed() {
</StyledSentMessage>
</StyledForm>
<Footer>
- <AppButton.ButtonGroup>
- <AppButton.BlueButton onClick={handleEditMessage}>
- {messages.pgettext('support-view', 'Edit message')}
- </AppButton.BlueButton>
- <AppButton.GreenButton onClick={onSend}>
- {messages.pgettext('support-view', 'Try again')}
- </AppButton.GreenButton>
- </AppButton.ButtonGroup>
+ <FlexColumn $gap="medium">
+ <Button onClick={handleEditMessage}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button text to edit the message after a failed attempt to send the problem report.
+ messages.pgettext('support-view', 'Edit message')
+ }
+ </Button.Text>
+ </Button>
+ <Button variant="success" onClick={onSend}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for retrying problem report submission after a failure.
+ messages.pgettext('support-view', 'Try again')
+ }
+ </Button.Text>
+ </Button>
+ </FlexColumn>
</Footer>
</StyledContent>
);
@@ -301,12 +312,17 @@ function NoEmailDialog() {
type={ModalAlertType.warning}
message={message}
buttons={[
- <AppButton.RedButton key="proceed" onClick={onSend}>
- {messages.pgettext('support-view', 'Send anyway')}
- </AppButton.RedButton>,
- <AppButton.BlueButton key="cancel" onClick={onCancelNoEmailDialog}>
- {messages.gettext('Back')}
- </AppButton.BlueButton>,
+ <Button variant="destructive" key="proceed" onClick={onSend}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for sending the problem report without an email address.
+ messages.pgettext('support-view', 'Send anyway')
+ }
+ </Button.Text>
+ </Button>,
+ <Button key="cancel" onClick={onCancelNoEmailDialog}>
+ <Button.Text>{messages.gettext('Back')}</Button.Text>
+ </Button>,
]}
close={onCancelNoEmailDialog}
/>
@@ -347,25 +363,31 @@ function OutdatedVersionWarningDialog() {
type={ModalAlertType.warning}
message={message}
buttons={[
- <AriaDescriptionGroup key="upgrade">
- <AriaDescribed>
- <AppButton.GreenButton disabled={isOffline} onClick={openDownloadLink}>
- <AppButton.Label>{messages.pgettext('support-view', 'Upgrade app')}</AppButton.Label>
- <AriaDescription>
- <Icon
- icon="external"
- aria-label={messages.pgettext('accessibility', 'Opens externally')}
- />
- </AriaDescription>
- </AppButton.GreenButton>
- </AriaDescribed>
- </AriaDescriptionGroup>,
- <AppButton.RedButton key="proceed" onClick={acknowledgeOutdatedVersion}>
- {messages.pgettext('support-view', 'Continue anyway')}
- </AppButton.RedButton>,
- <AppButton.BlueButton key="cancel" onClick={outdatedVersionCancel}>
- {messages.gettext('Cancel')}
- </AppButton.BlueButton>,
+ <Button
+ key="upgrade"
+ variant="success"
+ disabled={isOffline}
+ onClick={openDownloadLink}
+ aria-description={messages.pgettext('accessibility', 'Opens externally')}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for upgrading the app to the latest version.
+ messages.pgettext('support-view', 'Upgrade app')
+ }
+ </Button.Text>
+ <Button.Icon icon="external" />
+ </Button>,
+ <Button variant="destructive" key="proceed" onClick={acknowledgeOutdatedVersion}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for continuing problem report submission with an outdated app version.
+ messages.pgettext('support-view', 'Continue anyway')
+ }
+ </Button.Text>
+ </Button>,
+ <Button key="cancel" onClick={outdatedVersionCancel}>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>,
]}
close={pop}
/>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ProxyForm.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ProxyForm.tsx
index f37afdb5a2..e8e47bf15d 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ProxyForm.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ProxyForm.tsx
@@ -1,6 +1,4 @@
-import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
-import React from 'react';
-import styled from 'styled-components';
+import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import {
CustomProxy,
@@ -11,6 +9,8 @@ import {
Socks5RemoteCustomProxy,
} from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
+import { Button, Flex } from '../lib/components';
+import { FlexRow } from '../lib/components/flex-row';
import { IpAddress } from '../lib/ip';
import { useEffectEvent } from '../lib/utility-hooks';
import * as Cell from './cell';
@@ -20,12 +20,6 @@ import { SettingsRadioGroup } from './cell/SettingsRadioGroup';
import { SettingsRow } from './cell/SettingsRow';
import { SettingsSelect, SettingsSelectItem } from './cell/SettingsSelect';
import { SettingsNumberInput, SettingsTextInput } from './cell/SettingsTextInput';
-import {
- SmallButton,
- SmallButtonColor,
- SmallButtonGroup,
- SmallButtonGroupStart,
-} from './SmallButton';
interface ProxyFormContext {
proxy?: CustomProxy;
@@ -157,34 +151,31 @@ interface ProxyFormButtonsProps {
new: boolean;
}
-// TODO: Temporary fix, should be replaced with a flex or shared component
-const ActionGroup = styled.div({
- display: 'flex',
- gap: '12px',
-});
-
export function ProxyFormButtons(props: ProxyFormButtonsProps) {
const { onSave, onCancel, onDelete } = useContext(proxyFormContext);
// Contains form submittability to know whether or not to enable the Add/Save button.
const formSubmittable = useSettingsFormSubmittable();
-
return (
- <SmallButtonGroup>
- {onDelete !== undefined && (
- <SmallButtonGroupStart>
- <SmallButton color={SmallButtonColor.red} onClick={onDelete}>
- {messages.gettext('Delete')}
- </SmallButton>
- </SmallButtonGroupStart>
+ <Flex $margin={{ horizontal: 'medium', vertical: 'large' }} $justifyContent="space-between">
+ {onDelete !== undefined ? (
+ <Button width="fit" variant="destructive" onClick={onDelete}>
+ <Button.Text>{messages.gettext('Delete')}</Button.Text>
+ </Button>
+ ) : (
+ <div />
)}
- <ActionGroup>
- <SmallButton onClick={onCancel}>{messages.gettext('Cancel')}</SmallButton>
- <SmallButton onClick={onSave} disabled={!formSubmittable}>
- {props.new ? messages.gettext('Add') : messages.gettext('Save')}
- </SmallButton>
- </ActionGroup>
- </SmallButtonGroup>
+ <FlexRow $gap="small">
+ <Button width="fit" onClick={onCancel}>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>
+ <Button onClick={onSave} disabled={!formSubmittable}>
+ <Button.Text>
+ {props.new ? messages.gettext('Add') : messages.gettext('Save')}
+ </Button.Text>
+ </Button>
+ </FlexRow>
+ </Flex>
);
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/RedeemVoucher.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/RedeemVoucher.tsx
index b0d35343fa..9f875f9c04 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/RedeemVoucher.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/RedeemVoucher.tsx
@@ -6,10 +6,9 @@ import { VoucherResponse } from '../../shared/daemon-rpc-types';
import { formatRelativeDate } from '../../shared/date-helper';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
-import { Flex, Spinner } from '../lib/components';
+import { Button, ButtonProps, Flex, Spinner } from '../lib/components';
import { IconBadge } from '../lib/icon-badge';
import { useSelector } from '../redux/store';
-import * as AppButton from './AppButton';
import { ModalAlert } from './Modal';
import {
StyledEmptyResponse,
@@ -194,9 +193,14 @@ export function RedeemVoucherSubmitButton() {
const disabled = submitting || response?.type === 'success';
return (
- <AppButton.GreenButton disabled={!valueValid || disabled} onClick={onSubmit}>
- {messages.pgettext('redeem-voucher-view', 'Redeem')}
- </AppButton.GreenButton>
+ <Button variant="success" disabled={!valueValid || disabled} onClick={onSubmit}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for voucher redemption.
+ messages.pgettext('redeem-voucher-view', 'Redeem')
+ }
+ </Button.Text>
+ </Button>
);
}
@@ -220,9 +224,9 @@ export function RedeemVoucherAlert(props: IRedeemVoucherAlertProps) {
<ModalAlert
isOpen={props.show}
buttons={[
- <AppButton.BlueButton key="gotit" onClick={props.onClose}>
- {messages.gettext('Got it!')}
- </AppButton.BlueButton>,
+ <Button key="gotit" onClick={props.onClose}>
+ <Button.Text>{messages.gettext('Got it!')}</Button.Text>
+ </Button>,
]}
close={props.onClose}>
<Flex $justifyContent="center" $margin={{ top: 'large', bottom: 'medium' }}>
@@ -245,12 +249,22 @@ export function RedeemVoucherAlert(props: IRedeemVoucherAlertProps) {
isOpen={props.show}
buttons={[
<RedeemVoucherSubmitButton key="submit" />,
- <AppButton.BlueButton key="cancel" disabled={submitting} onClick={props.onClose}>
- {messages.pgettext('redeem-voucher-alert', 'Cancel')}
- </AppButton.BlueButton>,
+ <Button key="cancel" disabled={submitting} onClick={props.onClose}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Cancel button label for voucher redemption.
+ messages.pgettext('redeem-voucher-alert', 'Cancel')
+ }
+ </Button.Text>
+ </Button>,
]}
close={props.onClose}>
- <StyledLabel>{messages.pgettext('redeem-voucher-alert', 'Enter voucher code')}</StyledLabel>
+ <StyledLabel>
+ {
+ // TRANSLATORS: Input field label for voucher code.
+ messages.pgettext('redeem-voucher-alert', 'Enter voucher code')
+ }
+ </StyledLabel>
<RedeemVoucherInput />
<RedeemVoucherResponse />
</ModalAlert>
@@ -258,11 +272,9 @@ export function RedeemVoucherAlert(props: IRedeemVoucherAlertProps) {
}
}
-interface IRedeemVoucherButtonProps {
- className?: string;
-}
+type RedeemVoucherButtonProps = ButtonProps;
-export function RedeemVoucherButton(props: IRedeemVoucherButtonProps) {
+export function RedeemVoucherButton(props: RedeemVoucherButtonProps) {
const [showAlert, setShowAlert] = useState(false);
const onClick = useCallback(() => setShowAlert(true), []);
@@ -270,9 +282,14 @@ export function RedeemVoucherButton(props: IRedeemVoucherButtonProps) {
return (
<>
- <AppButton.GreenButton onClick={onClick} className={props.className}>
- {messages.pgettext('redeem-voucher-alert', 'Redeem voucher')}
- </AppButton.GreenButton>
+ <Button variant="success" onClick={onClick} {...props}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for redeeming a voucher.
+ messages.pgettext('redeem-voucher-alert', 'Redeem voucher')
+ }
+ </Button.Text>
+ </Button>
<RedeemVoucherContainer>
<RedeemVoucherAlert show={showAlert} onClose={onClose} />
</RedeemVoucherContainer>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SettingsImport.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SettingsImport.tsx
index 4b0459a392..4f47426b83 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SettingsImport.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SettingsImport.tsx
@@ -6,7 +6,7 @@ import { messages } from '../../shared/gettext';
import { useScheduler } from '../../shared/scheduler';
import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
-import { Flex, Icon, IconProps } from '../lib/components';
+import { Button, Flex, Icon, IconProps } from '../lib/components';
import { Colors, spacings } from '../lib/foundations';
import { TransitionType, useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
@@ -14,16 +14,12 @@ import { useBoolean, useEffectEvent } from '../lib/utility-hooks';
import settingsImportActions from '../redux/settings-import/actions';
import { useSelector } from '../redux/store';
import { AppNavigationHeader } from './';
+import { ButtonGroup } from './ButtonGroup';
import { measurements, normalText, tinyText } from './common-styles';
import { BackAction } from './KeyboardNavigation';
import { Footer, Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
-import { SmallButton, SmallButtonColor, SmallButtonGrid } from './SmallButton';
-
-const StyledSmallButtonGrid = styled(SmallButtonGrid)({
- margin: `0 ${measurements.horizontalViewMargin}`,
-});
type ImportStatus = { successful: boolean } & ({ type: 'file'; name: string } | { type: 'text' });
@@ -149,37 +145,38 @@ export default function SettingsImport() {
</SettingsHeader>
<Flex $flexDirection="column" $flex={1}>
- <StyledSmallButtonGrid>
- <SmallButton onClick={navigateTextImport}>
- {messages.pgettext('settings-import', 'Import via text')}
- </SmallButton>
- <SmallButton onClick={importFile}>
- {messages.pgettext('settings-import', 'Import file')}
- </SmallButton>
- </StyledSmallButtonGrid>
+ <ButtonGroup $gap="small" $margin="medium">
+ <Button onClick={navigateTextImport}>
+ <Button.Text>
+ {messages.pgettext('settings-import', 'Import via text')}
+ </Button.Text>
+ </Button>
+ <Button onClick={importFile}>
+ <Button.Text>{messages.pgettext('settings-import', 'Import file')}</Button.Text>
+ </Button>
+ </ButtonGroup>
<SettingsImportStatus status={importStatus} />
</Flex>
<Footer>
- <SmallButton
- onClick={showClearDialog}
- color={SmallButtonColor.red}
- disabled={!activeOverrides}>
- {messages.pgettext('settings-import', 'Clear all overrides')}
- </SmallButton>
+ <Button variant="destructive" onClick={showClearDialog} disabled={!activeOverrides}>
+ <Button.Text>
+ {messages.pgettext('settings-import', 'Clear all overrides')}
+ </Button.Text>
+ </Button>
</Footer>
<ModalAlert
isOpen={clearDialogVisible}
type={ModalAlertType.warning}
gridButtons={[
- <SmallButton key="cancel" onClick={hideClearDialog}>
- {messages.gettext('Cancel')}
- </SmallButton>,
- <SmallButton key="confirm" onClick={confirmClear} color={SmallButtonColor.red}>
- {messages.gettext('Clear')}
- </SmallButton>,
+ <Button key="cancel" onClick={hideClearDialog}>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>,
+ <Button key="confirm" onClick={confirmClear} variant="destructive">
+ <Button.Text>{messages.gettext('Clear')}</Button.Text>
+ </Button>,
]}
close={hideClearDialog}
title={messages.pgettext('settings-import', 'Clear all overrides?')}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SmallButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SmallButton.tsx
deleted file mode 100644
index 0db3499f3e..0000000000
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SmallButton.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import React from 'react';
-import styled from 'styled-components';
-
-import { Colors } from '../lib/foundations';
-import { smallText } from './common-styles';
-
-export enum SmallButtonColor {
- blue,
- red,
- green,
-}
-
-function getButtonColors(color?: SmallButtonColor, disabled?: boolean) {
- switch (color) {
- case SmallButtonColor.red:
- return {
- background: disabled ? Colors.red60 : Colors.red,
- backgroundHover: disabled ? Colors.red60 : Colors.red80,
- };
- case SmallButtonColor.green:
- return {
- background: disabled ? Colors.green40 : Colors.green,
- backgroundHover: disabled ? Colors.green40 : Colors.green90,
- };
- default:
- return {
- background: disabled ? Colors.blue50 : Colors.blue,
- backgroundHover: disabled ? Colors.blue50 : Colors.blue60,
- };
- }
-}
-
-const BUTTON_GROUP_GAP = 12;
-
-interface StyledSmallButtonProps {
- $color?: SmallButtonColor;
- disabled?: boolean;
-}
-
-const StyledSmallButton = styled.button<StyledSmallButtonProps>(smallText, (props) => {
- const buttonColors = getButtonColors(props.$color, props.disabled);
-
- return {
- display: 'flex',
- minHeight: '32px',
- padding: '5px 16px',
- border: 'none',
- background: buttonColors.background,
- color: props.disabled ? Colors.white50 : Colors.white,
- borderRadius: '4px',
- marginLeft: `${BUTTON_GROUP_GAP}px`,
- alignItems: 'center',
- justifyContent: 'center',
-
- '&&:not(& + &&)': {
- marginLeft: '0px',
- },
-
- [`${SmallButtonGroupStart} &&`]: {
- marginLeft: 0,
- marginRight: `${BUTTON_GROUP_GAP}px`,
- },
-
- [`${SmallButtonGrid} &&`]: {
- flex: '1 0 auto',
- marginLeft: 0,
- minWidth: `calc(50% - ${BUTTON_GROUP_GAP / 2}px)`,
- maxWidth: '100%',
- },
-
- '&&:hover': {
- background: buttonColors.backgroundHover,
- },
- };
-});
-
-const StyledContent = styled.span({
- flex: '1 0 fit-content',
-});
-
-const StyledTextOffset = styled.span<{ $width: number }>((props) => ({
- display: 'flex',
- flex: `0 1 ${props.$width}px`,
-}));
-
-export interface MultiButtonCompatibleProps {
- className?: string;
- textOffset?: number;
-}
-
-interface SmallButtonProps
- extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'color'>,
- MultiButtonCompatibleProps {
- onClick: () => void;
- children: React.ReactNode;
- color?: SmallButtonColor;
-}
-
-export function SmallButton(props: SmallButtonProps) {
- const { color, textOffset, children, ...otherProps } = props;
- return (
- <StyledSmallButton $color={props.color} {...otherProps}>
- {textOffset && textOffset > 0 ? <StyledTextOffset $width={Math.abs(textOffset)} /> : null}
- <StyledContent>{children}</StyledContent>
- {textOffset && textOffset < 0 ? <StyledTextOffset $width={Math.abs(textOffset)} /> : null}
- </StyledSmallButton>
- );
-}
-
-export const SmallButtonGroup = styled.div<{ $noMarginTop?: boolean }>((props) => ({
- display: 'flex',
- justifyContent: 'end',
- margin: '0 23px',
- marginTop: props.$noMarginTop ? 0 : '30px',
-}));
-
-export const SmallButtonGroupStart = styled(SmallButtonGroup)({
- flex: 1,
- justifyContent: 'start',
- margin: 0,
-});
-
-export const SmallButtonGrid = styled.div({
- display: 'flex',
- flexWrap: 'wrap',
- columnGap: `${BUTTON_GROUP_GAP}px`,
- rowGap: `${BUTTON_GROUP_GAP}px`,
-});
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
index 68644cdfa1..ab2821d415 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
@@ -12,6 +12,7 @@ import { strings } from '../../shared/constants';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import { Button, Container, Flex, FootnoteMini, IconButton, Spinner } from '../lib/components';
+import { FlexColumn } from '../lib/components/flex-column';
import { Colors } from '../lib/foundations';
import { useHistory } from '../lib/history';
import { formatHtml } from '../lib/html-formatter';
@@ -20,7 +21,6 @@ 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';
import { CustomScrollbarsRef } from './CustomScrollbars';
import { BackAction } from './KeyboardNavigation';
@@ -30,7 +30,6 @@ import { ModalAlert, ModalAlertType } from './Modal';
import { NavigationContainer } from './NavigationContainer';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
import {
- StyledBrowseButton,
StyledCellButton,
StyledCellLabel,
StyledCellWarningIcon,
@@ -42,7 +41,6 @@ import {
StyledPageCover,
StyledSearchBar,
StyledSpinnerRow,
- WideSmallButton,
} from './SplitTunnelingSettingsStyles';
import Switch from './Switch';
@@ -186,15 +184,20 @@ function LinuxSplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsProps
</StyledNoResult>
)}
- <Flex $flexDirection="column" $gap="medium">
+ <FlexColumn $gap="medium">
{filteredApplications !== undefined && filteredApplications.length > 0 && (
<ApplicationList applications={filteredApplications} rowRenderer={rowRenderer} />
)}
- <StyledBrowseButton onClick={launchWithFilePicker}>
- {messages.pgettext('split-tunneling-view', 'Find another app')}
- </StyledBrowseButton>
- </Flex>
+ <Button onClick={launchWithFilePicker}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for browsing applications with split tunneling.
+ messages.pgettext('split-tunneling-view', 'Find another app')
+ }
+ </Button.Text>
+ </Button>
+ </FlexColumn>
<ModalAlert
isOpen={browseError !== undefined}
@@ -209,9 +212,9 @@ function LinuxSplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsProps
{ detailedErrorMessage: browseError },
)}
buttons={[
- <AppButton.BlueButton key="close" onClick={hideBrowseFailureDialog}>
- {messages.gettext('Close')}
- </AppButton.BlueButton>,
+ <Button key="close" onClick={hideBrowseFailureDialog}>
+ <Button.Text>{messages.gettext('Close')}</Button.Text>
+ </Button>,
]}
close={hideBrowseFailureDialog}
/>
@@ -260,17 +263,22 @@ function LinuxApplicationRow(props: ILinuxApplicationRowProps) {
);
const warningDialogButtons = disabled
? [
- <AppButton.BlueButton key="cancel" onClick={hideWarningDialog}>
- {messages.gettext('Back')}
- </AppButton.BlueButton>,
+ <Button key="cancel" onClick={hideWarningDialog}>
+ <Button.Text>{messages.gettext('Back')}</Button.Text>
+ </Button>,
]
: [
- <AppButton.BlueButton key="launch" onClick={launch}>
- {messages.pgettext('split-tunneling-view', 'Launch')}
- </AppButton.BlueButton>,
- <AppButton.BlueButton key="cancel" onClick={hideWarningDialog}>
- {messages.gettext('Cancel')}
- </AppButton.BlueButton>,
+ <Button key="launch" onClick={launch}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for launching an application with split tunneling.
+ messages.pgettext('split-tunneling-view', 'Launch')
+ }
+ </Button.Text>
+ </Button>,
+ <Button key="cancel" onClick={hideWarningDialog}>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>,
];
return (
@@ -573,9 +581,11 @@ function MacOsSplitTunnelingAvailability({
</HeaderSubTitle>
<Flex $flexDirection="column" $gap="small">
<Flex $flexDirection="column" $gap="big">
- <WideSmallButton onClick={showFullDiskAccessSettings}>
- {messages.pgettext('split-tunneling-view', 'Open System Settings')}
- </WideSmallButton>
+ <Button onClick={showFullDiskAccessSettings}>
+ <Button.Text>
+ {messages.pgettext('split-tunneling-view', 'Open System Settings')}
+ </Button.Text>
+ </Button>
<FootnoteMini color={Colors.white60}>
{messages.pgettext(
'split-tunneling-view',
@@ -583,9 +593,11 @@ function MacOsSplitTunnelingAvailability({
)}
</FootnoteMini>
</Flex>
- <WideSmallButton onClick={restartDaemon}>
- {messages.pgettext('split-tunneling-view', 'Restart Mullvad Service')}
- </WideSmallButton>
+ <Button onClick={restartDaemon}>
+ <Button.Text>
+ {messages.pgettext('split-tunneling-view', 'Restart Mullvad Service')}
+ </Button.Text>
+ </Button>
</Flex>
</Flex>
);
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx
index 9cdc24ff31..0c4a200baa 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettingsStyles.tsx
@@ -1,12 +1,10 @@
import styled from 'styled-components';
import { Colors, spacings } from '../lib/foundations';
-import * as AppButton from './AppButton';
import * as Cell from './cell';
import { measurements, normalText } from './common-styles';
import { NavigationScrollbars } from './NavigationScrollbars';
import SearchBar from './SearchBar';
-import { SmallButton } from './SmallButton';
export const StyledPageCover = styled.div<{ $show: boolean }>((props) => ({
position: 'absolute',
@@ -70,10 +68,6 @@ export const StyledSpinnerRow = styled(Cell.CellButton)({
background: Colors.blue40,
});
-export const StyledBrowseButton = styled(AppButton.BlueButton)({
- margin: `0 ${measurements.horizontalViewMargin} ${measurements.verticalViewMargin}`,
-});
-
export const StyledNoResult = styled(Cell.CellFooter)({
display: 'flex',
flexDirection: 'column',
@@ -91,7 +85,3 @@ export const StyledSearchBar = styled(SearchBar)({
marginRight: measurements.horizontalViewMargin,
marginBottom: measurements.buttonVerticalMargin,
});
-
-export const WideSmallButton = styled(SmallButton)({
- width: '100%',
-});
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx
index 6ae2dd2166..3c011fd872 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx
@@ -8,6 +8,7 @@ import log from '../../shared/logging';
import { capitalizeEveryWord } from '../../shared/string-helpers';
import { useAppContext } from '../context';
import { Button, Flex, IconButton, Spinner } from '../lib/components';
+import { FlexColumn } from '../lib/components/flex-column';
import { Colors } from '../lib/foundations';
import { TransitionType, useHistory } from '../lib/history';
import { formatHtml } from '../lib/html-formatter';
@@ -16,7 +17,6 @@ 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';
@@ -129,7 +129,7 @@ export default function TooManyDevices() {
{devices !== undefined && (
<Footer>
- <AppButton.ButtonGroup>
+ <FlexColumn $gap="medium">
<Button
variant="success"
onClick={continueLogin}
@@ -144,7 +144,7 @@ export default function TooManyDevices() {
<Button onClick={cancel}>
<Button.Text>{messages.gettext('Back')}</Button.Text>
</Button>
- </AppButton.ButtonGroup>
+ </FlexColumn>
</Footer>
)}
</StyledContainer>
@@ -258,15 +258,17 @@ function Device(props: IDeviceProps) {
type={ModalAlertType.warning}
iconColor={Colors.red}
buttons={[
- <AppButton.RedButton key="remove" onClick={onRemove} disabled={deleting}>
- {
- // TRANSLATORS: Confirmation button when logging out other device.
- messages.pgettext('device-management', 'Yes, log out device')
- }
- </AppButton.RedButton>,
- <AppButton.BlueButton key="back" onClick={hideConfirmation} disabled={deleting}>
- {messages.gettext('Back')}
- </AppButton.BlueButton>,
+ <Button variant="destructive" key="remove" onClick={onRemove} disabled={deleting}>
+ <Button.Text>
+ {
+ // TRANSLATORS: Button label for confirming logout of another device.
+ messages.pgettext('device-management', 'Yes, log out device')
+ }
+ </Button.Text>
+ </Button>,
+ <Button key="back" onClick={hideConfirmation} disabled={deleting}>
+ <Button.Text>{messages.gettext('Back')}</Button.Text>
+ </Button>,
]}
close={hideConfirmation}>
<ModalMessage>
@@ -290,9 +292,9 @@ function Device(props: IDeviceProps) {
type={ModalAlertType.warning}
iconColor={Colors.red}
buttons={[
- <AppButton.BlueButton key="close" onClick={resetError}>
- {messages.gettext('Close')}
- </AppButton.BlueButton>,
+ <Button key="close" onClick={resetError}>
+ <Button.Text>{messages.gettext('Close')}</Button.Text>
+ </Button>,
]}
close={resetError}
message={messages.pgettext('device-management', 'Failed to remove device')}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx
index f0388c8a3e..af344f4aef 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/VpnSettings.tsx
@@ -7,6 +7,7 @@ import { IDnsOptions, TunnelProtocol } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import log from '../../shared/logging';
import { useAppContext } from '../context';
+import { Button } from '../lib/components';
import { useRelaySettingsUpdater } from '../lib/constraint-updater';
import { Colors, spacings } from '../lib/foundations';
import { useHistory } from '../lib/history';
@@ -17,7 +18,6 @@ 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';
@@ -548,9 +548,9 @@ function KillSwitchInfo() {
isOpen={killSwitchInfoVisible}
type={ModalAlertType.info}
buttons={[
- <AppButton.BlueButton key="back" onClick={hideKillSwitchInfo}>
- {messages.gettext('Got it!')}
- </AppButton.BlueButton>,
+ <Button key="back" onClick={hideKillSwitchInfo}>
+ <Button.Text>{messages.gettext('Got it!')}</Button.Text>
+ </Button>,
]}
close={hideKillSwitchInfo}>
<ModalMessage>
@@ -639,12 +639,12 @@ function LockdownMode() {
isOpen={confirmationDialogVisible}
type={ModalAlertType.caution}
buttons={[
- <AppButton.RedButton key="confirm" onClick={confirmLockdownMode}>
- {messages.gettext('Enable anyway')}
- </AppButton.RedButton>,
- <AppButton.BlueButton key="back" onClick={hideConfirmationDialog}>
- {messages.gettext('Back')}
- </AppButton.BlueButton>,
+ <Button variant="destructive" key="confirm" onClick={confirmLockdownMode}>
+ <Button.Text>{messages.gettext('Enable anyway')}</Button.Text>
+ </Button>,
+ <Button key="back" onClick={hideConfirmationDialog}>
+ <Button.Text>{messages.gettext('Back')}</Button.Text>
+ </Button>,
]}
close={hideConfirmationDialog}>
<ModalMessage>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/main-view/SelectLocationButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/main-view/SelectLocationButton.tsx
index 7581941942..3d78cbe6dd 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/main-view/SelectLocationButton.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/main-view/SelectLocationButton.tsx
@@ -130,7 +130,7 @@ function ReconnectButton(props: ButtonProps) {
return (
<StyledReconnectButton
onClick={onReconnect}
- size="auto"
+ width="fit"
aria-label={messages.gettext('Reconnect')}
{...props}>
<Icon icon="reconnect" />
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/CustomListDialogs.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/CustomListDialogs.tsx
index b22f0e3a91..78710fe44f 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/CustomListDialogs.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/CustomListDialogs.tsx
@@ -11,11 +11,11 @@ import {
import { messages } from '../../../shared/gettext';
import log from '../../../shared/logging';
import { useAppContext } from '../../context';
+import { Button } from '../../lib/components';
import { Colors } from '../../lib/foundations';
import { formatHtml } from '../../lib/html-formatter';
import { useBoolean } from '../../lib/utility-hooks';
import { useSelector } from '../../redux/store';
-import * as AppButton from '../AppButton';
import * as Cell from '../cell';
import { normalText, tinyText } from '../common-styles';
import { ModalAlert, ModalAlertType, ModalMessage } from '../Modal';
@@ -76,9 +76,9 @@ export function AddToListDialog(props: AddToListDialogProps) {
<ModalAlert
isOpen={props.isOpen}
buttons={[
- <AppButton.BlueButton key="cancel" onClick={props.hide}>
- {messages.gettext('Cancel')}
- </AppButton.BlueButton>,
+ <Button key="cancel" onClick={props.hide}>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>,
]}
close={props.hide}>
<StyledModalMessage>
@@ -188,12 +188,12 @@ export function EditListDialog(props: EditListProps) {
<ModalAlert
isOpen={props.isOpen}
buttons={[
- <AppButton.BlueButton key="save" disabled={!newNameValid} onClick={save}>
- {messages.gettext('Save')}
- </AppButton.BlueButton>,
- <AppButton.BlueButton key="cancel" onClick={props.hide}>
- {messages.gettext('Cancel')}
- </AppButton.BlueButton>,
+ <Button key="save" disabled={!newNameValid} onClick={save}>
+ <Button.Text>{messages.gettext('Save')}</Button.Text>
+ </Button>,
+ <Button key="cancel" onClick={props.hide}>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>,
]}
close={props.hide}>
<StyledModalMessage>
@@ -236,12 +236,12 @@ export function DeleteConfirmDialog(props: DeleteConfirmDialogProps) {
type={ModalAlertType.warning}
isOpen={props.isOpen}
buttons={[
- <AppButton.RedButton key="save" onClick={confirm}>
- {messages.gettext('Delete list')}
- </AppButton.RedButton>,
- <AppButton.BlueButton key="cancel" onClick={props.hide}>
- {messages.gettext('Cancel')}
- </AppButton.BlueButton>,
+ <Button key="save" variant="destructive" onClick={confirm}>
+ <Button.Text>{messages.gettext('Delete list')}</Button.Text>
+ </Button>,
+ <Button key="cancel" onClick={props.hide}>
+ <Button.Text>{messages.gettext('Cancel')}</Button.Text>
+ </Button>,
]}
close={props.hide}>
<ModalMessage>
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 c7af9a306b..381ba6495f 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,7 +4,8 @@ import { sprintf } from 'sprintf-js';
import { strings } from '../../../shared/constants';
import { Ownership } from '../../../shared/daemon-rpc-types';
import { messages } from '../../../shared/gettext';
-import { FilterChip, Flex, IconButton, LabelTiny } from '../../lib/components';
+import { Button, FilterChip, Flex, IconButton, LabelTiny } from '../../lib/components';
+import { FlexColumn } from '../../lib/components/flex-column';
import { useRelaySettingsUpdater } from '../../lib/constraint-updater';
import { daitaFilterActive, filterSpecialLocations } from '../../lib/filter-locations';
import { useHistory } from '../../lib/history';
@@ -33,7 +34,6 @@ import { LocationType, SpecialBridgeLocationType, SpecialLocation } from './sele
import { useSelectLocationContext } from './SelectLocationContainer';
import {
StyledContent,
- StyledDaitaSettingsButton,
StyledNavigationBarAttachment,
StyledScopeBar,
StyledSearchBar,
@@ -409,7 +409,7 @@ function DisabledEntrySelection() {
}, [push]);
return (
- <StyledSelectionUnavailable>
+ <FlexColumn $gap="large" $margin={{ horizontal: 'large', bottom: 'tiny' }}>
<StyledSelectionUnavailableText>
{sprintf(
messages.pgettext(
@@ -419,11 +419,13 @@ function DisabledEntrySelection() {
{ daita: strings.daita, multihop, directOnly },
)}
</StyledSelectionUnavailableText>
- <StyledDaitaSettingsButton onClick={navigateToDaitaSettings}>
- {sprintf(messages.pgettext('select-location-view', 'Open %(daita)s settings'), {
- daita: strings.daita,
- })}
- </StyledDaitaSettingsButton>
- </StyledSelectionUnavailable>
+ <Button onClick={navigateToDaitaSettings}>
+ <Button.Text>
+ {sprintf(messages.pgettext('select-location-view', 'Open %(daita)s settings'), {
+ daita: strings.daita,
+ })}
+ </Button.Text>
+ </Button>
+ </FlexColumn>
);
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocationStyles.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocationStyles.tsx
index 5ab08368a3..8064a05c56 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocationStyles.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocationStyles.tsx
@@ -4,7 +4,6 @@ import { Colors } from '../../lib/foundations';
import * as Cell from '../cell';
import { normalText, tinyText } from '../common-styles';
import SearchBar from '../SearchBar';
-import { SmallButton } from '../SmallButton';
import { ScopeBar } from './ScopeBar';
export const StyledContent = styled.div({
@@ -66,8 +65,3 @@ export const StyledSelectionUnavailableText = styled(Cell.CellFooterText)({
export const StyledAllLocationsTitle = styled(Cell.Label)(normalText, {
fontWeight: 'normal',
});
-
-export const StyledDaitaSettingsButton = styled(SmallButton)({
- marginLeft: 0,
- marginTop: '24px',
-});
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/Button.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/Button.tsx
index 5ce7593784..03481e2350 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/Button.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/Button.tsx
@@ -1,15 +1,14 @@
import React, { forwardRef } from 'react';
import styled, { css } from 'styled-components';
-import { Colors, Radius } from '../../foundations';
-import { Flex } from '../flex';
+import { Colors, Radius, spacings } from '../../foundations';
import { ButtonBase } from './ButtonBase';
import { ButtonProvider } from './ButtonContext';
-import { ButtonIcon, ButtonText, StyledIcon, StyledText } from './components';
+import { ButtonIcon, ButtonText, StyledButtonIcon, StyledButtonText } from './components';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'success' | 'destructive';
- size?: 'auto' | 'full' | '1/2';
+ width?: 'fill' | 'fit';
}
const styles = {
@@ -31,17 +30,21 @@ const styles = {
disabled: Colors.red60,
},
},
- sizes: {
- auto: 'auto',
- full: '100%',
- '1/2': '50%',
+ flex: {
+ fill: '1 1 0',
+ fit: '0 0 auto',
+ },
+ widths: {
+ fill: undefined,
+ fit: 'fit-content',
},
};
-const StyledButton = styled(ButtonBase)<ButtonProps>`
- ${({ size: sizeProp = 'full', variant: variantProp = 'primary' }) => {
+export const StyledButton = styled(ButtonBase)<ButtonProps>`
+ ${({ width: sizeProp = 'fill', variant: variantProp = 'primary' }) => {
const variant = styles.variants[variantProp];
- const size = styles.sizes[sizeProp];
+ const size = styles.flex[sizeProp];
+ const width = styles.widths[sizeProp];
return css`
--background: ${variant.background};
@@ -49,11 +52,18 @@ const StyledButton = styled(ButtonBase)<ButtonProps>`
--disabled: ${variant.disabled};
--radius: ${styles.radius};
--size: ${size};
+ --width: ${width};
+
+ display: flex;
+ flex: var(--size);
+ align-items: center;
+ padding: ${spacings.tiny} ${spacings.small};
+ gap: ${spacings.small};
min-height: 32px;
min-width: 60px;
+ width: var(--width);
border-radius: var(--radius);
- width: var(--size);
background: var(--background);
&:not(:disabled):hover {
@@ -68,62 +78,51 @@ const StyledButton = styled(ButtonBase)<ButtonProps>`
outline: 2px solid ${Colors.white};
outline-offset: 2px;
}
+
+ justify-content: space-between;
+ &&:has(${StyledButtonText}:only-child) {
+ justify-content: center;
+ }
+ &&:has(${StyledButtonText} + ${StyledButtonIcon}) {
+ &::before {
+ content: ' ';
+ display: inline-block;
+ width: 24px;
+ }
+ }
+ &&:has(${StyledButtonIcon} + ${StyledButtonText}) {
+ &::after {
+ content: ' ';
+ display: inline-block;
+ width: 24px;
+ }
+ }
+ &&:has(${StyledButtonIcon} + ${StyledButtonText} + ${StyledButtonIcon}) {
+ &::before {
+ display: none;
+ }
+ &::after {
+ display: none;
+ }
+ }
`;
}}
`;
-const StyledFlex = styled(Flex)`
- justify-content: space-between;
- &&:has(${StyledText}:only-child) {
- justify-content: center;
- }
- &&:has(${StyledText} + ${StyledIcon}) {
- &::before {
- content: ' ';
- display: inline-block;
- width: 24px;
- }
- }
- &&:has(${StyledIcon} + ${StyledText}) {
- &::after {
- content: ' ';
- display: inline-block;
- width: 24px;
- }
- }
- &&:has(${StyledIcon} + ${StyledText} + ${StyledIcon}) {
- &::before {
- display: none;
- }
- &::after {
- display: none;
- }
- }
-`;
-
-const Button = forwardRef<HTMLButtonElement, ButtonProps>(
- ({ children, disabled = false, style, ...props }, ref) => {
- return (
- <ButtonProvider disabled={disabled}>
- <StyledButton ref={ref} disabled={disabled} {...props}>
- <StyledFlex
- $flex={1}
- $gap="small"
- $alignItems="center"
- $padding={{
- horizontal: 'small',
- }}>
- {children}
- </StyledFlex>
- </StyledButton>
- </ButtonProvider>
- );
- },
-);
-
-Button.displayName = 'Button';
+const ForwardedButton = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
+ { children, disabled = false, style, ...props },
+ ref,
+) {
+ return (
+ <ButtonProvider disabled={disabled}>
+ <StyledButton ref={ref} disabled={disabled} {...props}>
+ {children}
+ </StyledButton>
+ </ButtonProvider>
+ );
+});
-const ButtonNamespace = Object.assign(Button, {
+const ButtonNamespace = Object.assign(ForwardedButton, {
Text: ButtonText,
Icon: ButtonIcon,
});
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonContext.tsx
index 4ff4876723..296b000bf4 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonContext.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonContext.tsx
@@ -19,6 +19,6 @@ interface ButtonProviderProps {
children: React.ReactNode;
}
-export const ButtonProvider = ({ disabled, children }: ButtonProviderProps) => {
+export function ButtonProvider({ disabled, children }: ButtonProviderProps) {
return <ButtonContext.Provider value={{ disabled }}>{children}</ButtonContext.Provider>;
-};
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonIcon.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonIcon.tsx
index fcf0959a82..b8a6d0582a 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonIcon.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonIcon.tsx
@@ -1,11 +1,21 @@
import styled from 'styled-components';
+import { Colors } from '../../../foundations';
import { Icon, IconProps } from '../../icon';
+import { useButtonContext } from '../ButtonContext';
type ButtonIconProps = Omit<IconProps, 'size'>;
-export const StyledIcon = styled(Icon)({});
+export const StyledButtonIcon = styled(Icon)({});
-export const ButtonIcon = ({ ...props }: ButtonIconProps) => {
- return <StyledIcon size="medium" {...props} />;
-};
+export function ButtonIcon({ ...props }: ButtonIconProps) {
+ const { disabled } = useButtonContext();
+ return (
+ <StyledButtonIcon
+ size="medium"
+ aria-hidden="true"
+ color={disabled ? Colors.white40 : Colors.white}
+ {...props}
+ />
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonText.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonText.tsx
index a1a3c02b3a..a55a48e637 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonText.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonText.tsx
@@ -6,9 +6,9 @@ import { BodySmallSemiBold, BodySmallSemiBoldProps } from '../../typography';
import { useButtonContext } from '../ButtonContext';
export type ButtonTextProps<T extends React.ElementType = 'span'> = BodySmallSemiBoldProps<T>;
-export const StyledText = styled(BodySmallSemiBold)``;
+export const StyledButtonText = styled(BodySmallSemiBold)``;
-export const ButtonText = <T extends React.ElementType = 'span'>(props: ButtonTextProps<T>) => {
+export function ButtonText<T extends React.ElementType = 'span'>(props: ButtonTextProps<T>) {
const { disabled } = useButtonContext();
- return <StyledText color={disabled ? Colors.white40 : Colors.white} {...props} />;
-};
+ return <StyledButtonText color={disabled ? Colors.white40 : Colors.white} {...props} />;
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-column/FlexColumn.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-column/FlexColumn.tsx
new file mode 100644
index 0000000000..decbfe25d8
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-column/FlexColumn.tsx
@@ -0,0 +1,5 @@
+import { Flex, FlexProps } from '../flex';
+
+type FlexColumnProps = Omit<FlexProps, '$flexDirection'>;
+
+export const FlexColumn = (props: FlexColumnProps) => <Flex $flexDirection="column" {...props} />;
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-column/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-column/index.ts
new file mode 100644
index 0000000000..f22ac25a46
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-column/index.ts
@@ -0,0 +1 @@
+export * from './FlexColumn';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-row/FlexRow.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-row/FlexRow.tsx
new file mode 100644
index 0000000000..c83152b2bc
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-row/FlexRow.tsx
@@ -0,0 +1,5 @@
+import { Flex, FlexProps } from '../flex';
+
+type FlexRowProps = Omit<FlexProps, '$flexDirection'>;
+
+export const FlexRow = (props: FlexRowProps) => <Flex $flexDirection="row" {...props} />;
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-row/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-row/index.ts
new file mode 100644
index 0000000000..053a93b058
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/flex-row/index.ts
@@ -0,0 +1 @@
+export * from './FlexRow';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/hooks/use-exclusive-task.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/hooks/use-exclusive-task.tsx
new file mode 100644
index 0000000000..01ea6acabf
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/hooks/use-exclusive-task.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { useCallback, useState } from 'react';
+
+export const useExclusiveTask = (task: () => Promise<void>) => {
+ const [running, setRunning] = useState(false);
+
+ const run = useCallback(async (): Promise<void | undefined> => {
+ if (running) {
+ return;
+ }
+ setRunning(true);
+ try {
+ await task();
+ } finally {
+ setRunning(false);
+ }
+ }, [task, running]);
+
+ const result = React.useMemo(() => [run, running] as const, [run, running]);
+
+ return result;
+};