summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOliver <oliver@mohlin.dev>2025-07-25 10:29:06 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-22 12:35:43 +0200
commitb30390fbddc768512a3b850cb19f8fa15ffe5669 (patch)
treea2eb4a56ead7ca6ece693415d72bd6cb8211a1e1
parent2444347e63eb48cea554facedd8568f0ead0ff7f (diff)
downloadmullvadvpn-b30390fbddc768512a3b850cb19f8fa15ffe5669.tar.xz
mullvadvpn-b30390fbddc768512a3b850cb19f8fa15ffe5669.zip
Add Switch component
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/Switch.tsx31
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/index.ts4
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/SwitchContext.tsx32
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/SwitchLabel.tsx14
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/SwitchThumb.tsx59
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/index.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBackgroundColor.ts12
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBorderColor.ts8
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/SwitchTrigger.tsx37
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/index.ts1
14 files changed, 204 insertions, 0 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/Switch.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/Switch.tsx
new file mode 100644
index 0000000000..748c689a9c
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/Switch.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+
+import { SwitchLabel, SwitchProvider, SwitchThumb, SwitchTrigger } from './components';
+
+export interface SwitchProps {
+ checked?: boolean;
+ onCheckedChange?: (checked: boolean) => void;
+ disabled?: boolean;
+ children: React.ReactNode;
+}
+
+function Switch({ checked, onCheckedChange, disabled, children }: SwitchProps) {
+ const labelId = React.useId();
+ return (
+ <SwitchProvider
+ labelId={labelId}
+ checked={checked}
+ onCheckedChange={onCheckedChange}
+ disabled={disabled}>
+ {children}
+ </SwitchProvider>
+ );
+}
+
+const SwitchNamespace = Object.assign(Switch, {
+ Label: SwitchLabel,
+ Thumb: SwitchThumb,
+ Trigger: SwitchTrigger,
+});
+
+export { SwitchNamespace as Switch };
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/index.ts
new file mode 100644
index 0000000000..54c6e9e6b9
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/index.ts
@@ -0,0 +1,4 @@
+export * from './switch-thumb';
+export * from './switch-trigger';
+export * from './switch-label';
+export * from './switch-context';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/SwitchContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/SwitchContext.tsx
new file mode 100644
index 0000000000..6e798e24a2
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/SwitchContext.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+
+import { SwitchProps } from '../../Switch';
+
+interface SwitchContextProps {
+ labelId: string;
+ disabled: SwitchProps['disabled'];
+ checked?: SwitchProps['checked'];
+ onCheckedChange?: SwitchProps['onCheckedChange'];
+}
+
+const SwitchContext = React.createContext<SwitchContextProps | undefined>(undefined);
+
+export const useSwitchContext = (): SwitchContextProps => {
+ const context = React.useContext(SwitchContext);
+ if (!context) {
+ throw new Error('useSwitchContext must be used within a SwitchProvider');
+ }
+ return context;
+};
+
+interface SwitchProviderProps {
+ labelId: string;
+ disabled: SwitchProps['disabled'];
+ checked?: SwitchProps['checked'];
+ onCheckedChange?: SwitchProps['onCheckedChange'];
+ children: React.ReactNode;
+}
+
+export function SwitchProvider({ children, ...props }: SwitchProviderProps) {
+ return <SwitchContext.Provider value={props}>{children}</SwitchContext.Provider>;
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/index.ts
new file mode 100644
index 0000000000..d4faa81ef4
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/index.ts
@@ -0,0 +1 @@
+export * from './SwitchContext';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/SwitchLabel.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/SwitchLabel.tsx
new file mode 100644
index 0000000000..9350cf2804
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/SwitchLabel.tsx
@@ -0,0 +1,14 @@
+import { Text, TextProps } from '../../../typography';
+import { useSwitchContext } from '../switch-context';
+
+export type SwitchLabelProps = TextProps;
+
+export function SwitchLabel({ children, ...props }: SwitchLabelProps) {
+ const { labelId, disabled } = useSwitchContext();
+
+ return (
+ <Text id={labelId} color={disabled ? 'whiteAlpha40' : 'white'} {...props}>
+ {children}
+ </Text>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/index.ts
new file mode 100644
index 0000000000..fec6b853a8
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/index.ts
@@ -0,0 +1 @@
+export * from './SwitchLabel';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/SwitchThumb.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/SwitchThumb.tsx
new file mode 100644
index 0000000000..2a365b6b63
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/SwitchThumb.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import styled, { css } from 'styled-components';
+
+import { colors } from '../../../../foundations';
+import { Dot } from '../../../dot';
+import { useSwitchContext } from '../switch-context';
+import { useBackgroundColor, useBorderColor } from './hooks';
+
+export type SwitchThumbProps = React.HtmlHTMLAttributes<HTMLDivElement>;
+
+const StyledSwitchThumbIndicator = styled(Dot)<{
+ $checked?: boolean;
+ $backgroundColor?: string;
+}>`
+ ${({ $checked, $backgroundColor }) => {
+ return css`
+ position: absolute;
+ left: 2px;
+ background-color: ${$backgroundColor};
+ transform: translateX(${$checked ? '11px' : '1px'});
+ transition:
+ width 150ms ease,
+ height 150ms ease,
+ transform 150ms ease,
+ background-color 100ms linear;
+ `;
+ }}
+`;
+
+const StyledSwitchThumbTrack = styled.div<{ $borderColor: string }>`
+ ${({ $borderColor }) => {
+ return css`
+ position: relative;
+ display: flex;
+ align-items: center;
+ width: 32px;
+ height: 20px;
+ border: 2px solid ${$borderColor};
+ border-radius: 100px;
+ transition: border-color 200ms ease;
+
+ &:focus-visible {
+ outline: 2px solid ${colors.white};
+ outline-offset: 2px;
+ }
+ `;
+ }}
+`;
+
+export function SwitchThumb(props: SwitchThumbProps) {
+ const { checked } = useSwitchContext();
+ const backgroundColor = useBackgroundColor();
+ const borderColor = useBorderColor();
+ return (
+ <StyledSwitchThumbTrack $borderColor={colors[borderColor]} {...props}>
+ <StyledSwitchThumbIndicator $checked={checked} $backgroundColor={colors[backgroundColor]} />
+ </StyledSwitchThumbTrack>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/index.ts
new file mode 100644
index 0000000000..56a7f81dd4
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from './useBackgroundColor';
+export * from './useBorderColor';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBackgroundColor.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBackgroundColor.ts
new file mode 100644
index 0000000000..6f80aee0ba
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBackgroundColor.ts
@@ -0,0 +1,12 @@
+import { Colors } from '../../../../../foundations';
+import { useSwitchContext } from '../../switch-context';
+
+export const useBackgroundColor = (): Colors => {
+ const { disabled, checked } = useSwitchContext();
+ if (disabled) {
+ if (checked) return 'green40';
+ else return 'red40';
+ }
+ if (checked) return 'green';
+ else return 'red';
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBorderColor.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBorderColor.ts
new file mode 100644
index 0000000000..37c270a86e
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBorderColor.ts
@@ -0,0 +1,8 @@
+import { Colors } from '../../../../../foundations';
+import { useSwitchContext } from '../../switch-context';
+
+export const useBorderColor = (): Colors => {
+ const { disabled } = useSwitchContext();
+ if (disabled) return 'whiteAlpha20';
+ return 'whiteAlpha80';
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/index.ts
new file mode 100644
index 0000000000..4f3b2c8b31
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/index.ts
@@ -0,0 +1 @@
+export * from './SwitchThumb';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/SwitchTrigger.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/SwitchTrigger.tsx
new file mode 100644
index 0000000000..cbef07c6ff
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/SwitchTrigger.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import styled from 'styled-components';
+
+import { colors } from '../../../../foundations';
+import { useSwitchContext } from '../switch-context';
+
+export type SwitchTriggerProps = React.ComponentPropsWithRef<'button'>;
+
+export const StyledSwitchTrigger = styled.button<{ $checked?: boolean }>`
+ background-color: transparent;
+ width: fit-content;
+
+ &&:focus-visible {
+ outline: 2px solid ${colors.white};
+ outline-offset: -1px;
+ }
+`;
+
+export function SwitchTrigger(props: SwitchTriggerProps) {
+ const { labelId, checked, disabled, onCheckedChange } = useSwitchContext();
+ const handleClick = React.useCallback(() => {
+ if (onCheckedChange) {
+ onCheckedChange(!checked);
+ }
+ }, [checked, onCheckedChange]);
+
+ return (
+ <StyledSwitchTrigger
+ onClick={handleClick}
+ disabled={disabled}
+ role="switch"
+ aria-checked={checked ? 'true' : 'false'}
+ aria-labelledby={labelId}
+ {...props}
+ />
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/index.ts
new file mode 100644
index 0000000000..a32b52020b
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/index.ts
@@ -0,0 +1 @@
+export * from './SwitchTrigger';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/index.ts
new file mode 100644
index 0000000000..1b19c1d39c
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/index.ts
@@ -0,0 +1 @@
+export * from './Switch';