aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-02-17 03:17:49 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-03-06 18:56:45 +0100
commit2efa902d42733abafcfcacd6ae8efcea4afe59ae (patch)
tree168f7f0e1e806b30af68701cff4350bd96f9751c
parentfeat: Basic location bar (diff)
downloadsophie-2efa902d42733abafcfcacd6ae8efcea4afe59ae.tar.gz
sophie-2efa902d42733abafcfcacd6ae8efcea4afe59ae.tar.zst
sophie-2efa902d42733abafcfcacd6ae8efcea4afe59ae.zip
design: Location bar and other UI styling
InputBase paddings are idiosyncratic if there is both a start and an end adornment. To simplify the styles, we override the styling from InputBase and compute our own paddings. The animated color change when switching from a secure site to an insecure one was distracting, so we disable color animations in the location bar.
-rw-r--r--packages/renderer/src/components/ThemeProvider.tsx12
-rw-r--r--packages/renderer/src/components/locationBar/ButtonAdornment.tsx69
-rw-r--r--packages/renderer/src/components/locationBar/GoAdornment.tsx38
-rw-r--r--packages/renderer/src/components/locationBar/IconAdornment.tsx41
-rw-r--r--packages/renderer/src/components/locationBar/LocationBar.tsx6
-rw-r--r--packages/renderer/src/components/locationBar/LocationOverlayInput.tsx110
-rw-r--r--packages/renderer/src/components/locationBar/LocationTextField.tsx85
-rw-r--r--packages/renderer/src/components/locationBar/NavigationButtons.tsx16
-rw-r--r--packages/renderer/src/components/locationBar/UrlAdornment.tsx91
-rw-r--r--packages/renderer/src/components/locationBar/UrlOverlay.tsx70
-rw-r--r--packages/renderer/src/components/locationBar/splitUrl.ts62
-rw-r--r--packages/renderer/src/components/sidebar/ServiceSwitcher.tsx5
-rw-r--r--packages/renderer/src/components/sidebar/Sidebar.tsx19
-rw-r--r--packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx31
14 files changed, 615 insertions, 40 deletions
diff --git a/packages/renderer/src/components/ThemeProvider.tsx b/packages/renderer/src/components/ThemeProvider.tsx
index 3943371..5fc1f33 100644
--- a/packages/renderer/src/components/ThemeProvider.tsx
+++ b/packages/renderer/src/components/ThemeProvider.tsx
@@ -37,6 +37,18 @@ export default observer(
37 palette: { 37 palette: {
38 mode: shouldUseDarkColors ? 'dark' : 'light', 38 mode: shouldUseDarkColors ? 'dark' : 'light',
39 }, 39 },
40 components: {
41 MuiBadge: {
42 styleOverrides: {
43 standard: {
44 // Reduce badge with to make the unread message badge with "99+" unread messages
45 // fit within the sidebar. Applied for all badges for consistency.
46 paddingLeft: 4,
47 paddingRight: 4,
48 },
49 },
50 },
51 },
40 }); 52 });
41 53
42 return <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>; 54 return <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>;
diff --git a/packages/renderer/src/components/locationBar/ButtonAdornment.tsx b/packages/renderer/src/components/locationBar/ButtonAdornment.tsx
new file mode 100644
index 0000000..2cf230b
--- /dev/null
+++ b/packages/renderer/src/components/locationBar/ButtonAdornment.tsx
@@ -0,0 +1,69 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import InputAdornment from '@mui/material/InputAdornment';
22import { styled } from '@mui/material/styles';
23
24export const NO_LABEL_BUTTON_CLASS_NAME = 'ButtonAdornment-NoLabel';
25
26const ButtonAdornment = styled(InputAdornment, {
27 name: 'ButtonAdornment',
28})(({ theme, position }) => {
29 const { direction } = theme;
30 const left = direction === 'ltr' ? 'start' : 'end';
31 return {
32 ...(position === left
33 ? {
34 marginRight: 2,
35 marginLeft: -8,
36 }
37 : {
38 marginLeft: 2,
39 marginRight: -8,
40 }),
41 '.MuiButton-root': {
42 minWidth: 32,
43 height: 32,
44 paddingLeft: 6,
45 paddingRight: 6,
46 borderRadius: 16,
47 },
48 ...(direction === 'ltr'
49 ? {
50 '.MuiButton-startIcon': {
51 marginLeft: 0,
52 },
53 [`.${NO_LABEL_BUTTON_CLASS_NAME} .MuiButton-startIcon`]: {
54 marginRight: 0,
55 },
56 }
57 : {
58 '.MuiButton-startIcon': {
59 marginRight: 0,
60 marginLeft: theme.spacing(1),
61 },
62 [`.${NO_LABEL_BUTTON_CLASS_NAME} .MuiButton-startIcon`]: {
63 marginLeft: 0,
64 },
65 }),
66 };
67});
68
69export default ButtonAdornment;
diff --git a/packages/renderer/src/components/locationBar/GoAdornment.tsx b/packages/renderer/src/components/locationBar/GoAdornment.tsx
new file mode 100644
index 0000000..43c8b7b
--- /dev/null
+++ b/packages/renderer/src/components/locationBar/GoAdornment.tsx
@@ -0,0 +1,38 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import IconGo from '@mui/icons-material/Send';
22import Button from '@mui/material/Button';
23import React from 'react';
24
25import ButtonAdornment, { NO_LABEL_BUTTON_CLASS_NAME } from './ButtonAdornment';
26
27export default function GoAdornment(): JSX.Element {
28 return (
29 <ButtonAdornment position="end">
30 <Button
31 aria-label="Go"
32 color="inherit"
33 startIcon={<IconGo />}
34 className={NO_LABEL_BUTTON_CLASS_NAME}
35 />
36 </ButtonAdornment>
37 );
38}
diff --git a/packages/renderer/src/components/locationBar/IconAdornment.tsx b/packages/renderer/src/components/locationBar/IconAdornment.tsx
new file mode 100644
index 0000000..1a2fa83
--- /dev/null
+++ b/packages/renderer/src/components/locationBar/IconAdornment.tsx
@@ -0,0 +1,41 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import InputAdornment from '@mui/material/InputAdornment';
22import { styled } from '@mui/material/styles';
23
24const IconAdornment = styled(InputAdornment, {
25 name: 'IconAdornment',
26})(({ theme: { direction }, position }) => {
27 const left = direction === 'ltr' ? 'start' : 'end';
28 return {
29 ...(position === left
30 ? {
31 marginLeft: -2,
32 marginRight: 8,
33 }
34 : {
35 marginLeft: 8,
36 marginRight: -2,
37 }),
38 };
39});
40
41export default IconAdornment;
diff --git a/packages/renderer/src/components/locationBar/LocationBar.tsx b/packages/renderer/src/components/locationBar/LocationBar.tsx
index e1f470d..8b079f0 100644
--- a/packages/renderer/src/components/locationBar/LocationBar.tsx
+++ b/packages/renderer/src/components/locationBar/LocationBar.tsx
@@ -27,6 +27,8 @@ import { useStore } from '../StoreProvider';
27import LocationTextField from './LocationTextField'; 27import LocationTextField from './LocationTextField';
28import NavigationButtons from './NavigationButtons'; 28import NavigationButtons from './NavigationButtons';
29 29
30export const LOCATION_BAR_ID = 'Sophie-LocationBar';
31
30const LocationBarRoot = styled('header', { 32const LocationBarRoot = styled('header', {
31 name: 'LocationBar', 33 name: 'LocationBar',
32 slot: 'Root', 34 slot: 'Root',
@@ -34,6 +36,8 @@ const LocationBarRoot = styled('header', {
34 display: hidden ? 'none' : 'flex', 36 display: hidden ? 'none' : 'flex',
35 flexDirection: 'row', 37 flexDirection: 'row',
36 padding: theme.spacing(1), 38 padding: theme.spacing(1),
39 // Align the bottom border with the service switcher in the sidebar.
40 paddingBottom: `calc(${theme.spacing(1)} - 1px)`,
37 gap: theme.spacing(1), 41 gap: theme.spacing(1),
38 borderBottom: `1px solid ${theme.palette.divider}`, 42 borderBottom: `1px solid ${theme.palette.divider}`,
39})); 43}));
@@ -44,7 +48,7 @@ function LocationBar(): JSX.Element {
44 } = useStore(); 48 } = useStore();
45 49
46 return ( 50 return (
47 <LocationBarRoot id="locationBar" hidden={!showLocationBar}> 51 <LocationBarRoot id={LOCATION_BAR_ID} hidden={!showLocationBar}>
48 <NavigationButtons service={selectedService} /> 52 <NavigationButtons service={selectedService} />
49 <LocationTextField service={selectedService} /> 53 <LocationTextField service={selectedService} />
50 </LocationBarRoot> 54 </LocationBarRoot>
diff --git a/packages/renderer/src/components/locationBar/LocationOverlayInput.tsx b/packages/renderer/src/components/locationBar/LocationOverlayInput.tsx
new file mode 100644
index 0000000..55c1fb8
--- /dev/null
+++ b/packages/renderer/src/components/locationBar/LocationOverlayInput.tsx
@@ -0,0 +1,110 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import { styled } from '@mui/material/styles';
22import React, { forwardRef, ForwardedRef } from 'react';
23
24const inputClassName = 'LocationOverlayInput-Input';
25
26const overlayClassName = 'LocationOverlayInput-Overlay';
27
28const clipClassName = 'LocationOverlayInput-Clip';
29
30const LocationOverlayInputRoot = styled('div', {
31 name: 'LocationOverlayInput',
32 slot: 'Root',
33 shouldForwardProp: (prop) => prop !== 'overlayVisible',
34})<{ overlayVisible: boolean }>(({ theme, overlayVisible }) => {
35 const itemStyle = {
36 paddingLeft: 0,
37 paddingRight: 0,
38 };
39 return {
40 display: 'flex',
41 position: 'relative',
42 flex: 1,
43 [`.${inputClassName}`]: {
44 ...itemStyle,
45 color: overlayVisible ? 'transparent' : 'inherit',
46 },
47 [`.${overlayClassName}`]: {
48 ...itemStyle,
49 display: 'flex',
50 position: 'absolute',
51 left: 0,
52 right: 0,
53 top: 0,
54 bottom: 0,
55 // Text rendering with selected transparent text works better on the bottom in light mode.
56 zIndex: theme.palette.mode === 'dark' ? 999 : -999,
57 pointerEvents: 'none',
58 width: 'auto',
59 },
60 [`.${clipClassName}`]: {
61 flex: 1,
62 overflow: 'hidden',
63 whiteSpace: 'nowrap',
64 textOverflow: 'clip',
65 },
66 };
67});
68
69export interface LocationOverlayInputProps
70 extends React.HTMLProps<HTMLInputElement> {
71 className?: string | undefined;
72
73 overlayVisible?: boolean;
74
75 overlay?: JSX.Element | undefined;
76}
77
78const LocationOverlayInput = forwardRef(
79 (
80 { overlayVisible, overlay, className, ...props }: LocationOverlayInputProps,
81 ref: ForwardedRef<HTMLInputElement>,
82 ) => (
83 <LocationOverlayInputRoot overlayVisible={overlayVisible ?? false}>
84 {/* eslint-disable react/jsx-props-no-spreading --
85 Deliberately passing props through to the actual input element.
86 */}
87 <input
88 ref={ref}
89 className={`${className ?? ''} ${inputClassName}`}
90 {...props}
91 />
92 {/* eslint-enable react/jsx-props-no-spreading */}
93 {overlayVisible && (
94 <div aria-hidden className={`${className ?? ''} ${overlayClassName}`}>
95 <div className={clipClassName}>{overlay}</div>
96 </div>
97 )}
98 </LocationOverlayInputRoot>
99 ),
100);
101
102LocationOverlayInput.displayName = 'LocationOverlayInput';
103
104LocationOverlayInput.defaultProps = {
105 className: undefined,
106 overlayVisible: false,
107 overlay: undefined,
108};
109
110export default LocationOverlayInput;
diff --git a/packages/renderer/src/components/locationBar/LocationTextField.tsx b/packages/renderer/src/components/locationBar/LocationTextField.tsx
index a618bf6..f436bf0 100644
--- a/packages/renderer/src/components/locationBar/LocationTextField.tsx
+++ b/packages/renderer/src/components/locationBar/LocationTextField.tsx
@@ -18,40 +18,95 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import IconGlobe from '@mui/icons-material/Public';
22import FilledInput from '@mui/material/FilledInput'; 21import FilledInput from '@mui/material/FilledInput';
23import InputAdornment from '@mui/material/InputAdornment'; 22import { styled } from '@mui/material/styles';
24import { Service } from '@sophie/shared'; 23import { Service } from '@sophie/shared';
24import { autorun } from 'mobx';
25import { observer } from 'mobx-react-lite'; 25import { observer } from 'mobx-react-lite';
26import React from 'react'; 26import React, { useCallback, useEffect, useState } from 'react';
27
28import GoAdornment from './GoAdornment';
29import LocationOverlayInput from './LocationOverlayInput';
30import UrlAdornment from './UrlAdornment';
31import UrlOverlay from './UrlOverlay';
32import splitUrl from './splitUrl';
33
34const LocationTextFieldRoot = styled(FilledInput, {
35 name: 'LocationTextField',
36 slot: 'Root',
37})(({ theme }) => ({
38 paddingLeft: 12,
39 paddingRight: 12,
40 borderRadius: 20,
41 '&.Mui-focused': {
42 outline: `2px solid ${theme.palette.primary.main}`,
43 },
44}));
27 45
28function LocationTextField({ 46function LocationTextField({
29 service, 47 service,
30}: { 48}: {
31 service: Service | undefined; 49 service: Service | undefined;
32}): JSX.Element { 50}): JSX.Element {
51 const [inputFocused, setInputFocused] = useState(false);
52 const [changed, setChanged] = useState(false);
53 const [value, setValue] = useState('');
54
55 const resetValue = useCallback(() => {
56 setValue(service?.currentUrl ?? '');
57 setChanged(false);
58 }, [service, setChanged, setValue]);
59
60 useEffect(
61 () =>
62 autorun(() => {
63 resetValue();
64 }),
65 [resetValue],
66 );
67
68 const inputRefCallback = useCallback(
69 (input: HTMLInputElement) => {
70 setInputFocused(
71 document.activeElement !== null && document.activeElement === input,
72 );
73 },
74 [setInputFocused],
75 );
76
77 const splitResult = splitUrl(service?.currentUrl);
78
33 return ( 79 return (
34 <FilledInput 80 <LocationTextFieldRoot
35 aria-label="Location" 81 inputComponent={LocationOverlayInput}
36 inputProps={{ 82 inputProps={{
83 'aria-label': 'Location',
37 spellCheck: false, 84 spellCheck: false,
85 overlayVisible: !inputFocused && !changed,
86 overlay: <UrlOverlay splitResult={splitResult} />,
87 }}
88 inputRef={inputRefCallback}
89 onFocus={() => setInputFocused(true)}
90 onBlur={() => setInputFocused(false)}
91 onChange={({ target: { value: newValue } }) => {
92 setValue(newValue);
93 setChanged(true);
94 }}
95 onKeyUp={(event) => {
96 if (event.key === 'Escape') {
97 resetValue();
98 event.preventDefault();
99 }
38 }} 100 }}
39 size="small" 101 size="small"
40 fullWidth 102 fullWidth
41 hiddenLabel 103 hiddenLabel
42 disableUnderline 104 disableUnderline
43 sx={(theme) => ({
44 borderRadius: 1,
45 '&.Mui-focused': {
46 outline: `2px solid ${theme.palette.primary.main}`,
47 },
48 })}
49 startAdornment={ 105 startAdornment={
50 <InputAdornment position="start"> 106 <UrlAdornment changed={changed} splitResult={splitResult} />
51 <IconGlobe fontSize="small" />
52 </InputAdornment>
53 } 107 }
54 value={service?.currentUrl ?? ''} 108 endAdornment={changed ? <GoAdornment /> : undefined}
109 value={value}
55 /> 110 />
56 ); 111 );
57} 112}
diff --git a/packages/renderer/src/components/locationBar/NavigationButtons.tsx b/packages/renderer/src/components/locationBar/NavigationButtons.tsx
index 82ed657..77b02b6 100644
--- a/packages/renderer/src/components/locationBar/NavigationButtons.tsx
+++ b/packages/renderer/src/components/locationBar/NavigationButtons.tsx
@@ -23,6 +23,7 @@ import IconArrowForward from '@mui/icons-material/ArrowForward';
23import IconStop from '@mui/icons-material/Close'; 23import IconStop from '@mui/icons-material/Close';
24import IconHome from '@mui/icons-material/HomeOutlined'; 24import IconHome from '@mui/icons-material/HomeOutlined';
25import IconRefresh from '@mui/icons-material/Refresh'; 25import IconRefresh from '@mui/icons-material/Refresh';
26import { useTheme } from '@mui/material';
26import Box from '@mui/material/Box'; 27import Box from '@mui/material/Box';
27import IconButton from '@mui/material/IconButton'; 28import IconButton from '@mui/material/IconButton';
28import { Service } from '@sophie/shared'; 29import { Service } from '@sophie/shared';
@@ -34,6 +35,8 @@ function NavigationButtons({
34}: { 35}: {
35 service: Service | undefined; 36 service: Service | undefined;
36}): JSX.Element { 37}): JSX.Element {
38 const { direction } = useTheme();
39
37 return ( 40 return (
38 <Box 41 <Box
39 sx={{ 42 sx={{
@@ -42,16 +45,16 @@ function NavigationButtons({
42 }} 45 }}
43 > 46 >
44 <IconButton 47 <IconButton
45 aria-label="Go back" 48 aria-label="Back"
46 disabled={service === undefined || !service.canGoBack} 49 disabled={service === undefined || !service.canGoBack}
47 > 50 >
48 <IconArrowBack /> 51 {direction === 'ltr' ? <IconArrowBack /> : <IconArrowForward />}
49 </IconButton> 52 </IconButton>
50 <IconButton 53 <IconButton
51 aria-label="Go forward" 54 aria-label="Forward"
52 disabled={service === undefined || !service.canGoForward} 55 disabled={service === undefined || !service.canGoForward}
53 > 56 >
54 <IconArrowForward /> 57 {direction === 'ltr' ? <IconArrowForward /> : <IconArrowBack />}
55 </IconButton> 58 </IconButton>
56 {service?.state === 'loading' ? ( 59 {service?.state === 'loading' ? (
57 <IconButton aria-label="Stop"> 60 <IconButton aria-label="Stop">
@@ -62,10 +65,7 @@ function NavigationButtons({
62 <IconRefresh /> 65 <IconRefresh />
63 </IconButton> 66 </IconButton>
64 )} 67 )}
65 <IconButton 68 <IconButton aria-label="Home" disabled={service === undefined}>
66 aria-label="Go to service homepage"
67 disabled={service === undefined}
68 >
69 <IconHome /> 69 <IconHome />
70 </IconButton> 70 </IconButton>
71 </Box> 71 </Box>
diff --git a/packages/renderer/src/components/locationBar/UrlAdornment.tsx b/packages/renderer/src/components/locationBar/UrlAdornment.tsx
new file mode 100644
index 0000000..6ede378
--- /dev/null
+++ b/packages/renderer/src/components/locationBar/UrlAdornment.tsx
@@ -0,0 +1,91 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import IconHttps from '@mui/icons-material/HttpsOutlined';
22import IconGlobe from '@mui/icons-material/Public';
23import IconWarning from '@mui/icons-material/Warning';
24import { styled } from '@mui/material';
25import Button from '@mui/material/Button';
26import React from 'react';
27
28import ButtonAdornment, { NO_LABEL_BUTTON_CLASS_NAME } from './ButtonAdornment';
29import IconAdornment from './IconAdornment';
30import type { SplitResult } from './splitUrl';
31
32const FastColorChangingButton = styled(Button)(({ theme }) => ({
33 transition: theme.transitions.create(
34 ['background-color', 'box-shadow', 'border-color'],
35 {
36 duration: theme.transitions.duration.short,
37 easing: theme.transitions.easing.easeInOut,
38 },
39 ),
40}));
41
42export default function UrlAdornment({
43 splitResult,
44 changed,
45}: {
46 splitResult: SplitResult;
47 changed: boolean;
48}): JSX.Element {
49 const { type } = splitResult;
50 if (changed || type === 'empty') {
51 return (
52 <IconAdornment position="start">
53 <IconGlobe fontSize="small" />
54 </IconAdornment>
55 );
56 }
57 switch (type) {
58 case 'valid': {
59 const { secure } = splitResult;
60 return secure ? (
61 <ButtonAdornment position="start">
62 <FastColorChangingButton
63 aria-label="Show certificate"
64 color="inherit"
65 className={NO_LABEL_BUTTON_CLASS_NAME}
66 startIcon={<IconHttps />}
67 />
68 </ButtonAdornment>
69 ) : (
70 <ButtonAdornment position="start">
71 <FastColorChangingButton color="error" startIcon={<IconWarning />}>
72 Not secure
73 </FastColorChangingButton>
74 </ButtonAdornment>
75 );
76 }
77 case 'invalid':
78 return (
79 <ButtonAdornment position="start">
80 <FastColorChangingButton color="error" startIcon={<IconWarning />}>
81 Unknown site
82 </FastColorChangingButton>
83 </ButtonAdornment>
84 );
85 default:
86 /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions --
87 Error handling for impossible case.
88 */
89 throw new Error(`Unexpected SplitResult: ${type}`);
90 }
91}
diff --git a/packages/renderer/src/components/locationBar/UrlOverlay.tsx b/packages/renderer/src/components/locationBar/UrlOverlay.tsx
new file mode 100644
index 0000000..f7a3c4c
--- /dev/null
+++ b/packages/renderer/src/components/locationBar/UrlOverlay.tsx
@@ -0,0 +1,70 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import { styled } from '@mui/material/styles';
22import React from 'react';
23
24import type { SplitResult } from './splitUrl';
25
26const PrimaryFragment = styled('span', {
27 name: 'LocationOverlayInput',
28 slot: 'PrimaryFragment',
29 shouldForwardProp: (prop) => prop !== 'alert',
30})<{ alert: boolean }>(({ theme, alert }) => ({
31 color: alert ? theme.palette.error.main : theme.palette.text.primary,
32}));
33
34const SecondaryFragment = styled('span', {
35 name: 'LocationOverlayInput',
36 slot: 'SecondaryFragment',
37})(({ theme }) => ({
38 color: theme.palette.text.secondary,
39}));
40
41export default function UrlOverlay({
42 splitResult,
43}: {
44 splitResult: SplitResult;
45}): JSX.Element {
46 const { type } = splitResult;
47 switch (type) {
48 case 'valid': {
49 const { secure, prefix, host, suffix } = splitResult;
50 return (
51 <>
52 <SecondaryFragment>{prefix}</SecondaryFragment>
53 <PrimaryFragment alert={!secure}>{host}</PrimaryFragment>
54 <SecondaryFragment>{suffix}</SecondaryFragment>
55 </>
56 );
57 }
58 case 'invalid': {
59 const { value } = splitResult;
60 return <PrimaryFragment alert>{value}</PrimaryFragment>;
61 }
62 case 'empty':
63 return <PrimaryFragment alert={false} />;
64 default:
65 /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions --
66 Error handling for impossible case.
67 */
68 throw new Error(`Unexpected SplitResult: ${type}`);
69 }
70}
diff --git a/packages/renderer/src/components/locationBar/splitUrl.ts b/packages/renderer/src/components/locationBar/splitUrl.ts
new file mode 100644
index 0000000..6e95472
--- /dev/null
+++ b/packages/renderer/src/components/locationBar/splitUrl.ts
@@ -0,0 +1,62 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21export type SplitResult =
22 | {
23 type: 'valid';
24 secure: boolean;
25 prefix: string;
26 host: string;
27 suffix: string;
28 }
29 | {
30 type: 'invalid';
31 value: string;
32 }
33 | {
34 type: 'empty';
35 };
36
37export default function splitUrl(urlString: string | undefined): SplitResult {
38 if (urlString === undefined || urlString === '') {
39 return { type: 'empty' };
40 }
41 let url: URL;
42 try {
43 url = new URL(urlString);
44 } catch {
45 return { type: 'invalid', value: urlString };
46 }
47 const { protocol, host, username, password, pathname, search, hash } = url;
48 if (protocol === 'http:' || protocol === 'https:') {
49 return {
50 type: 'valid',
51 secure: protocol === 'https:',
52 prefix: `${protocol}//${
53 username === ''
54 ? ''
55 : `${username}${password === '' ? '' : `:${password}`}@`
56 }`,
57 host,
58 suffix: `${pathname}${search}${hash}`,
59 };
60 }
61 return { type: 'invalid', value: urlString };
62}
diff --git a/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx b/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx
index 3b47990..2241476 100644
--- a/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx
+++ b/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx
@@ -32,6 +32,7 @@ const ServiceSwitcherRoot = styled(Tabs, {
32 name: 'ServiceSwitcher', 32 name: 'ServiceSwitcher',
33 slot: 'Root', 33 slot: 'Root',
34})(({ theme }) => ({ 34})(({ theme }) => ({
35 // Move the indicator to the outer side of the window.
35 '.MuiTabs-indicator': { 36 '.MuiTabs-indicator': {
36 ...(theme.direction === 'ltr' 37 ...(theme.direction === 'ltr'
37 ? { 38 ? {
@@ -50,13 +51,15 @@ const ServiceSwitcherTab = styled(Tab, {
50 slot: 'Tab', 51 slot: 'Tab',
51})(({ theme }) => ({ 52})(({ theme }) => ({
52 minWidth: 0, 53 minWidth: 0,
54 paddingInlineStart: theme.spacing(2),
55 paddingInlineEnd: `calc(${theme.spacing(2)} - 1px)`,
53 transition: theme.transitions.create('background-color', { 56 transition: theme.transitions.create('background-color', {
54 duration: theme.transitions.duration.shortest, 57 duration: theme.transitions.duration.shortest,
55 }), 58 }),
56 '&.Mui-selected': { 59 '&.Mui-selected': {
57 backgroundColor: 60 backgroundColor:
58 theme.palette.mode === 'dark' 61 theme.palette.mode === 'dark'
59 ? alpha(theme.palette.text.primary, 0.12) 62 ? alpha(theme.palette.text.primary, 0.13)
60 : alpha(theme.palette.primary.light, 0.24), 63 : alpha(theme.palette.primary.light, 0.24),
61 }, 64 },
62})); 65}));
diff --git a/packages/renderer/src/components/sidebar/Sidebar.tsx b/packages/renderer/src/components/sidebar/Sidebar.tsx
index 80826ca..4f27d68 100644
--- a/packages/renderer/src/components/sidebar/Sidebar.tsx
+++ b/packages/renderer/src/components/sidebar/Sidebar.tsx
@@ -19,7 +19,6 @@
19 */ 19 */
20 20
21import Box from '@mui/material/Box'; 21import Box from '@mui/material/Box';
22import { alpha } from '@mui/material/styles';
23import React from 'react'; 22import React from 'react';
24 23
25import ServiceSwitcher from './ServiceSwitcher'; 24import ServiceSwitcher from './ServiceSwitcher';
@@ -36,21 +35,29 @@ export default function Sidebar(): JSX.Element {
36 flexDirection: 'column', 35 flexDirection: 'column',
37 alignItems: 'center', 36 alignItems: 'center',
38 paddingY: 1, 37 paddingY: 1,
38 paddingInlineStart: '1px',
39 gap: 1, 39 gap: 1,
40 background: alpha(theme.palette.text.primary, 0.09), 40 backgroundColor:
41 theme.palette.mode === 'dark'
42 ? 'rgba(255, 255, 255, 0.09)'
43 : 'rgba(0, 0, 0, 0.06)',
41 backgroundClip: 'padding-box', 44 backgroundClip: 'padding-box',
42 borderInlineEnd: `1px solid ${theme.palette.divider}`, 45 borderInlineEnd: `1px solid ${theme.palette.divider}`,
43 minWidth: 69, 46 minWidth: `calc(${theme.spacing(4)} + 36px)`,
44 })} 47 })}
45 > 48 >
46 <ToggleLocationBarButton /> 49 <ToggleLocationBarButton />
47 <ServiceSwitcher />
48 <Box 50 <Box
49 sx={{ 51 sx={{
50 flex: 1, 52 flex: 1,
51 WebkitAppRegion: 'drag', 53 display: 'flex',
54 flexDirection: 'column',
55 justifyContent: 'flex-start',
56 marginInlineStart: '-1px',
52 }} 57 }}
53 /> 58 >
59 <ServiceSwitcher />
60 </Box>
54 <ToggleDarkModeButton /> 61 <ToggleDarkModeButton />
55 </Box> 62 </Box>
56 ); 63 );
diff --git a/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
index 7e20598..60033b0 100644
--- a/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
+++ b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
@@ -20,12 +20,29 @@
20 20
21import IconChevronLeft from '@mui/icons-material/KeyboardDoubleArrowLeft'; 21import IconChevronLeft from '@mui/icons-material/KeyboardDoubleArrowLeft';
22import IconChevronRight from '@mui/icons-material/KeyboardDoubleArrowRight'; 22import IconChevronRight from '@mui/icons-material/KeyboardDoubleArrowRight';
23import { useTheme } from '@mui/material';
23import CircularProgress from '@mui/material/CircularProgress'; 24import CircularProgress from '@mui/material/CircularProgress';
24import IconButton from '@mui/material/IconButton'; 25import IconButton from '@mui/material/IconButton';
25import { observer } from 'mobx-react-lite'; 26import { observer } from 'mobx-react-lite';
26import React from 'react'; 27import React from 'react';
27 28
28import { useStore } from '../StoreProvider'; 29import { useStore } from '../StoreProvider';
30import { LOCATION_BAR_ID } from '../locationBar/LocationBar';
31
32function ToggleLocationBarIcon({
33 loading,
34 show,
35}: {
36 loading: boolean;
37 show: boolean;
38}): JSX.Element {
39 const { direction } = useTheme();
40 if (loading) {
41 return <CircularProgress color="inherit" size="1.5rem" />;
42 }
43 const left = direction === 'ltr' ? show : !show;
44 return left ? <IconChevronLeft /> : <IconChevronRight />;
45}
29 46
30function ToggleLocationBarButton(): JSX.Element { 47function ToggleLocationBarButton(): JSX.Element {
31 const store = useStore(); 48 const store = useStore();
@@ -33,21 +50,17 @@ function ToggleLocationBarButton(): JSX.Element {
33 settings: { selectedService, showLocationBar }, 50 settings: { selectedService, showLocationBar },
34 } = store; 51 } = store;
35 52
36 let icon: JSX.Element;
37 if (selectedService?.state === 'loading') {
38 icon = <CircularProgress color="inherit" size="1.5rem" />;
39 } else {
40 icon = showLocationBar ? <IconChevronLeft /> : <IconChevronRight />;
41 }
42
43 return ( 53 return (
44 <IconButton 54 <IconButton
45 aria-pressed={showLocationBar} 55 aria-pressed={showLocationBar}
46 aria-controls="locationBar" 56 aria-controls={LOCATION_BAR_ID}
47 aria-label="Show location bar" 57 aria-label="Show location bar"
48 onClick={() => store.toggleLocationBar()} 58 onClick={() => store.toggleLocationBar()}
49 > 59 >
50 {icon} 60 <ToggleLocationBarIcon
61 loading={selectedService?.state === 'loading'}
62 show={showLocationBar}
63 />
51 </IconButton> 64 </IconButton>
52 ); 65 );
53} 66}