Skip to content

Commit 1511cc2

Browse files
committed
frontend: bottom nav for mobile
created a bottom navigation bar and hide the sidebar on mobile for better mobile UX. Also created 2 new routes / page, the "all accounts" route and "more" route to support this new bottom navigation bar.
1 parent 45f54df commit 1511cc2

26 files changed

+631
-36
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44
- Reduced support for BitBox01
55
- Fix a bug that would prevent the app to perform firmware upgrade when offline.
6+
- Replace sidebar with bottom navigation bar for mobile devices
67

78
# v4.47.2
89
- Linux: fix compatiblity with some versions of Mesa that are incompatible with the bundled wayland libraries

frontends/web/src/app.module.css

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
display: flex;
33
flex: 1;
44
flex-direction: column;
5-
height: 100vh;
5+
height: 100dvh;
66
min-width: 0;
77
/* mobile viewport bug fix */
88
max-height: -webkit-fill-available;
@@ -13,4 +13,10 @@
1313
position: absolute;
1414
width: 100%;
1515
}
16+
}
17+
18+
@media (max-width: 768px) {
19+
.appContent.hasBottomNavigation {
20+
height: calc(100dvh - 84px);
21+
}
1622
}

frontends/web/src/app.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { Darkmode } from './components/darkmode/darkmode';
4242
import { AuthRequired } from './components/auth/authrequired';
4343
import { WCSigningRequest } from './components/wallet-connect/incoming-signing-request';
4444
import { Providers } from './contexts/providers';
45+
import { BottomNavigation } from './components/bottom-navigation/bottom-navigation';
4546
import styles from './app.module.css';
4647

4748
export const App = () => {
@@ -165,6 +166,9 @@ export const App = () => {
165166

166167
const deviceIDs: string[] = Object.keys(devices);
167168
const activeAccounts = accounts.filter(acct => acct.active);
169+
170+
const showBottomNavigation = deviceIDs.length > 0 || activeAccounts.length > 0;
171+
168172
return (
169173
<ConnectedApp>
170174
<Providers>
@@ -175,7 +179,7 @@ export const App = () => {
175179
accounts={activeAccounts}
176180
devices={devices}
177181
/>
178-
<div className={styles.appContent}>
182+
<div className={`${styles.appContent} ${showBottomNavigation ? styles.hasBottomNavigation : ''}`}>
179183
<WCSigningRequest />
180184
<Aopp />
181185
<KeystoreConnectPrompt />
@@ -202,6 +206,7 @@ export const App = () => {
202206
/>
203207
<RouterWatcher />
204208
</div>
209+
{showBottomNavigation && <BottomNavigation />}
205210
<Alert />
206211
<Confirm />
207212
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.container {
2+
background-color: var(--background-secondary);
3+
bottom: 0;
4+
border-top: 2px solid var(--bottom-navigation-border-color);
5+
display: none;
6+
padding: var(--space-half);
7+
position: fixed;
8+
width: 100%;
9+
height: 84px;
10+
justify-content: space-between;
11+
}
12+
13+
.link {
14+
align-items: center;
15+
color: var(--color-text);
16+
display: flex;
17+
font-size: 12px;
18+
flex-direction: column;
19+
gap: 4px;
20+
justify-content: center;
21+
text-align: center;
22+
text-decoration: none;
23+
}
24+
25+
.link svg {
26+
flex-shrink: 0;
27+
width: 24px;
28+
height: 24px;
29+
}
30+
31+
.link.active {
32+
color: var(--color-primary);
33+
text-decoration: underline;
34+
text-underline-offset: 7px;
35+
text-decoration-thickness: 3px;
36+
}
37+
38+
@media (max-width: 768px) {
39+
.container {
40+
display: flex;
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Copyright 2025 Shift Crypto AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { useTranslation } from 'react-i18next';
18+
import { Link, useLocation } from 'react-router-dom';
19+
import { AccountIconSVG, ExchangeIconSVG, MoreIconSVG, PortfolioIconSVG } from '@/components/bottom-navigation/menu-icons';
20+
import styles from './bottom-navigation.module.css';
21+
22+
export const BottomNavigation = () => {
23+
const { t } = useTranslation();
24+
const { pathname } = useLocation();
25+
26+
return (
27+
<div className={styles.container}>
28+
<Link
29+
className={`${styles.link} ${pathname.startsWith('/account-summary') ? styles.active : ''}`}
30+
to="/account-summary"
31+
>
32+
<PortfolioIconSVG />
33+
{t('accountSummary.portfolio')}
34+
</Link>
35+
<Link
36+
className={`${styles.link} ${pathname.startsWith('/account/') || pathname.startsWith('/accounts/') ? styles.active : ''}`}
37+
to="/accounts/all"
38+
>
39+
<AccountIconSVG />
40+
{t('settings.accounts')}
41+
</Link>
42+
<Link
43+
className={`${styles.link} ${pathname.startsWith('/exchange/') ? styles.active : ''}`}
44+
to="/exchange/info"
45+
>
46+
<ExchangeIconSVG />
47+
{t('generic.buySell')}
48+
</Link>
49+
<Link
50+
className={`${styles.link} ${pathname.startsWith('/settings/') || pathname.startsWith('/bitsurance/') ? styles.active : ''}`}
51+
to="/settings/more"
52+
>
53+
<MoreIconSVG />
54+
{t('settings.more')}
55+
</Link>
56+
</div>
57+
);
58+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
export const PortfolioIconSVG = () => (
3+
<svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
4+
<path d="M20.8125 18.5625H4.6875V3.9375C4.6875 3.83437 4.60313 3.75 4.5 3.75H3.1875C3.08437 3.75 3 3.83437 3 3.9375V20.0625C3 20.1656 3.08437 20.25 3.1875 20.25H20.8125C20.9156 20.25 21 20.1656 21 20.0625V18.75C21 18.6469 20.9156 18.5625 20.8125 18.5625ZM7.16719 14.9461C7.23984 15.0188 7.35703 15.0188 7.43203 14.9461L10.6734 11.7211L13.6641 14.7305C13.7367 14.8031 13.8562 14.8031 13.9259 14.7305L20.3836 8.24813C20.4563 8.20547 20.4563 8.08594 20.3836 8.01325L19.4555 7.08516C19.4202 7.05026 19.3726 7.03069 19.323 7.03069C19.2435 7.03069 19.2259 7.05026 19.1906 7.08516L13.8 12.4734L10.8141 9.46875C10.7788 9.43386 10.7312 9.41425 10.6816 9.41425C10.632 9.41425 10.5845 9.43386 10.5492 9.46875L6.24141 13.7508C6.20651 13.786 6.18694 13.8336 6.18694 13.8832C6.18694 13.9325 6.20651 13.9804 6.24141 14.0156L7.16719 14.9461Z" fill="currentColor"/>
5+
</svg>
6+
);
7+
8+
9+
export const AccountIconSVG = () => (
10+
<svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
11+
<path d="M21.375 4.5H7.6875C7.58437 4.5 7.5 4.58437 7.5 4.6875V6C7.5 6.10313 7.58437 6.1875 7.6875 6.1875H21.375C21.4781 6.1875 21.5625 6.10313 21.5625 6V4.6875C21.5625 4.58437 21.4781 4.5 21.375 4.5ZM21.375 11.1562H7.6875C7.58437 11.1562 7.5 11.2406 7.5 11.3438V12.6562C7.5 12.7594 7.58437 12.8438 7.6875 12.8438H21.375C21.4781 12.8438 21.5625 12.7594 21.5625 12.6562V11.3438C21.5625 11.2406 21.4781 11.1562 21.375 11.1562ZM21.375 17.8125H7.6875C7.58437 17.8125 7.5 17.8969 7.5 18V19.3125C7.5 19.4156 7.58437 19.5 7.6875 19.5H21.375C21.4781 19.5 21.5625 19.4156 21.5625 19.3125V18C21.5625 17.8969 21.4781 17.8125 21.375 17.8125ZM2.4375 5.34375C2.4375 5.51611 2.47145 5.68678 2.53741 5.84602C2.60337 6.00526 2.70005 6.14995 2.82192 6.24183C2.9438 6.3937 3.08849 6.49038 3.24773 6.55634C3.40697 6.6223 3.57764 6.65625 3.75 6.65625C3.92236 6.65625 4.09303 6.6223 4.25224 6.55634C4.41151 6.49038 4.5562 6.3937 4.67808 6.24183C4.79995 6.14995 4.89663 6.00526 4.96259 5.84602C5.02555 5.68678 5.0625 5.51611 5.0625 5.34375C5.0625 5.17139 5.02555 5.00072 4.96259 4.84148C4.89663 4.68224 4.79995 4.53755 4.67808 4.41567C4.5562 4.2938 4.41151 4.19712 4.25224 4.13116C4.09303 4.0652 3.92236 4.03125 3.75 4.03125C3.57764 4.03125 3.40697 4.0652 3.24773 4.13116C3.08849 4.19712 2.9438 4.2938 2.82192 4.41567C2.70005 4.53755 2.60337 4.68224 2.53741 4.84148C2.47145 5.00072 2.4375 5.17139 2.4375 5.34375V5.34375ZM2.4375 12C2.4375 12.1724 2.47145 12.343 2.53741 12.5023C2.60337 12.6615 2.70005 12.8062 2.82192 12.9251C2.9438 13.05 3.08849 13.1466 3.24773 13.2126C3.40697 13.2486 3.57764 13.3125 3.75 13.3125C3.92236 13.3125 4.09303 13.2486 4.25224 13.2126C4.41151 13.1466 4.5562 13.05 4.67808 12.9251C4.79995 12.8062 4.89663 12.6615 4.96259 12.5023C5.02555 12.343 5.0625 12.1724 5.0625 12C5.0625 11.8246 5.02555 11.657 4.96259 11.4977C4.89663 11.3385 4.79995 11.1938 4.67808 11.0719C4.5562 10.95 4.41151 10.8534 4.25224 10.7874C4.09303 10.7214 3.92236 10.6875 3.75 10.6875C3.57764 10.6875 3.40697 10.7214 3.24773 10.7874C3.08849 10.8534 2.9438 10.95 2.82192 11.0719C2.70005 11.1938 2.60337 11.3385 2.53741 11.4977C2.47145 11.657 2.4375 11.8246 2.4375 12V12ZM2.4375 18.6562C2.4375 18.8256 2.47145 18.9993 2.53741 19.1585C2.60337 19.3178 2.70005 19.4625 2.82192 19.5843C2.9438 19.7062 3.08849 19.8029 3.24773 19.8688C3.40697 19.9348 3.57764 19.9688 3.75 19.9688C3.92236 19.9688 4.09303 19.9348 4.25224 19.8688C4.41151 19.8029 4.5562 19.7062 4.67808 19.5843C4.79995 19.4625 4.89663 19.3178 4.96259 19.1585C5.02555 18.9993 5.0625 18.8256 5.0625 18.6562C5.0625 18.4839 5.02555 18.3132 4.96259 18.154C4.89663 17.9947 4.79995 17.85 4.67808 17.7252C4.5562 17.6063 4.41151 17.5096 4.25224 17.4437C4.09303 17.3777 3.92236 17.3438 3.75 17.3438C3.57764 17.3438 3.40697 17.3777 3.24773 17.4437C3.08849 17.5096 2.9438 17.6063 2.82192 17.7252C2.70005 17.85 2.60337 17.9947 2.53741 18.154C2.47145 18.3132 2.4375 18.4839 2.4375 18.6562V18.6562Z" fill="currentColor"/>
12+
</svg>
13+
);
14+
15+
export const ExchangeIconSVG = () => (
16+
<svg width="25" height="25" viewBox="0 0 14 18" fill="none" xmlns="http://www.w3.org/2000/svg">
17+
<path d="M10.9231 3.93519V2.2037C10.9231 0.787037 8.07692 0 5.46154 0C2.84615 0 0 0.787037 0 2.2037C0 2.36111 -7.45058e-08 2.51852 0.076923 2.59722H0V4.3257C0 5.35185 1.38462 5.98148 3.07692 6.2963V8.02478C3.07692 8.18519 3.15385 8.34259 3.23077 8.5787C2 8.97222 1.30769 9.60185 1.30769 10.3102V14.7963C1.30769 16.213 4.15385 17 6.76923 17C9.38461 17 12.2308 16.213 12.2308 14.7963V12.5926V10.2315C12.2308 10.0741 12.1538 9.91667 12.0769 9.68056C13.2308 9.25704 14 8.73611 14 7.94907V5.90248C14 5.03704 12.8462 4.25 10.9231 3.93519ZM5.46154 0.708333C8.38461 0.708333 10.2308 1.57407 10.2308 2.2037C10.2308 2.83333 8.38461 3.69907 5.46154 3.69907C2.53846 3.69907 0.692308 2.83333 0.692308 2.2037C0.692308 1.57407 2.53846 0.708333 5.46154 0.708333ZM0.692308 4.40741V3.30556C1.69231 4.01389 3.61538 4.40741 5.46154 4.40741C7.30769 4.40741 9.23077 4.01389 10.2308 3.30556V4.40741C10.2308 5.03704 8.38461 5.90248 5.46154 5.90248C2.53846 5.90248 0.692308 5.03704 0.692308 4.40741ZM11.5385 14.7963C11.5385 15.4259 9.69231 16.2917 6.76923 16.2917C3.84615 16.2917 2 15.4259 2 14.7963V13.6157C3 14.3241 4.92308 14.7176 6.76923 14.7176C8.61538 14.7176 10.5385 14.3241 11.5385 13.6157V14.7963ZM11.5385 12.4352C11.5385 13.0648 9.69231 13.9306 6.76923 13.9306C3.84615 13.9306 2 13.0648 2 12.4352V11.3333C3 12.0417 4.92308 12.4352 6.76923 12.4352C8.61538 12.4352 10.5385 12.0417 11.5385 11.3333V12.4352ZM6.76923 11.8056C3.84615 11.8056 2 10.9398 2 10.3102C2 9.99537 2.53846 9.52315 3.76923 9.20833C4.76923 9.91667 6.69231 10.3102 8.53846 10.3102C9.53846 10.3102 10.4615 10.2315 11.3846 9.99537C11.4615 10.0741 11.5385 10.2315 11.5385 10.3102C11.5385 10.9398 9.69231 11.8056 6.76923 11.8056ZM13.3077 8.10648C13.3077 8.73611 11.4615 9.60185 8.53846 9.60185C5.61538 9.60185 3.76923 8.73611 3.76923 8.10648V7.00463C4.76923 7.71296 6.69231 8.10648 8.53846 8.10648C10.3846 8.10648 12.3077 7.71296 13.3077 7.00463V8.10648ZM8.53846 7.39815C6.53846 7.39815 5 7.00463 4.30769 6.53241C4.69231 6.53241 5.07692 6.61111 5.53846 6.61111C8 6.61111 10.6923 5.90248 10.9231 4.64352C12.4615 4.95833 13.3077 5.50926 13.3077 5.90248C13.3077 6.53241 11.4615 7.39815 8.53846 7.39815Z" fill="currentColor"/>
18+
</svg>
19+
);
20+
21+
export const MoreIconSVG = () => (
22+
<svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
23+
<path d="M4.125 11.9766C4.125 12.1489 4.15895 12.3196 4.22491 12.4788C4.29087 12.6381 4.38755 12.7828 4.50942 12.9046C4.6313 13.0265 4.77599 13.1232 4.93523 13.1892C5.09447 13.2551 5.26514 13.2891 5.4375 13.2891C5.60986 13.2891 5.78053 13.2551 5.93977 13.1892C6.09901 13.1232 6.2437 13.0265 6.36558 12.9046C6.48745 12.7828 6.58413 12.6381 6.65009 12.4788C6.71605 12.3196 6.75 12.1489 6.75 11.9766C6.75 11.8042 6.71605 11.6335 6.65009 11.4743C6.58413 11.3151 6.48745 11.1704 6.36558 11.0485C6.2437 10.9266 6.09901 10.8299 5.93977 10.764C5.78053 10.698 5.60986 10.6641 5.4375 10.6641C5.26514 10.6641 5.09447 10.698 4.93523 10.764C4.77599 10.8299 4.6313 10.9266 4.50942 11.0485C4.38755 11.1704 4.29087 11.3151 4.22491 11.4743C4.15895 11.6335 4.125 11.8042 4.125 11.9766V11.9766ZM10.6875 11.9766C10.6875 12.3247 10.8258 12.6585 11.0719 12.9046C11.3181 13.1508 11.6519 13.2891 12 13.2891C12.3481 13.2891 12.6819 13.1508 12.9281 12.9046C13.1742 12.6585 13.3125 12.3247 13.3125 11.9766C13.3125 11.6285 13.1742 11.2946 12.9281 11.0485C12.6819 10.8023 12.3481 10.6641 12 10.6641C11.6519 10.6641 11.3181 10.8023 11.0719 11.0485C10.8258 11.2946 10.6875 11.6285 10.6875 11.9766V11.9766ZM17.25 11.9766C17.25 12.3247 17.3883 12.6585 17.6344 12.9046C17.8806 13.1508 18.2144 13.2891 18.5625 13.2891C18.9106 13.2891 19.2444 13.1508 19.4906 12.9046C19.7367 12.6585 19.875 12.3247 19.875 11.9766C19.875 11.6285 19.7367 11.2946 19.4906 11.0485C19.2444 10.8023 18.9106 10.6641 18.5625 10.6641C18.2144 10.6641 17.8806 10.8023 17.6344 11.0485C17.3883 11.2946 17.25 11.6285 17.25 11.9766V11.9766Z" fill="currentColor"/>
24+
</svg>
25+
26+
);
Loading
Loading

frontends/web/src/components/icon/icon.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ import statusSuccessSVG from './assets/icons/icon-success.svg';
9595
import statusWarningSVG from './assets/icons/icon-warning.svg';
9696
import statusErrorSVG from './assets/icons/icon-error.svg';
9797
import plusSVG from './assets/icons/plus.svg';
98+
import cogSVG from './assets/icons/cog.svg';
9899
import style from './icon.module.css';
99100

100101
export const ExpandOpen = () => (
@@ -242,6 +243,7 @@ export const StatusInfo = (props: ImgProps) => (<img src={statusInfoSVG} draggab
242243
export const StatusWarning = (props: ImgProps) => (<img src={statusWarningSVG} draggable={false} {...props} />);
243244
export const StatusError = (props: ImgProps) => (<img src={statusErrorSVG} draggable={false} {...props} />);
244245
export const Plus = (props: ImgProps) => (<img src={plusSVG} draggable={false} {...props} />);
246+
export const Cog = (props: ImgProps) => (<img src={cogSVG} draggable={false} {...props} />);
245247
/**
246248
* @deprecated Alert is only used for BitBox01 use `Warning` icon instead
247249
*/

frontends/web/src/components/layout/header.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ export const Header = ({
4545
return false;
4646
};
4747

48+
4849
return (
4950
<div className={[style.container, sidebarStatus ? style[sidebarStatus] : ''].join(' ')}>
5051
<div className={style.header}>
51-
<div className={`${style.sidebarToggler} ${hideSidebarToggler ? style.hideSidebarToggler : ''}`} onClick={toggleSidebar}>
52+
<div className={`hide-on-small ${style.sidebarToggler} ${hideSidebarToggler ? style.hideSidebarToggler : ''}`} onClick={toggleSidebar}>
5253
<MenuDark className="show-in-lightmode" />
5354
<MenuLight className="show-in-darkmode" />
5455
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.button {
2+
border-width: 0;
3+
display: flex;
4+
min-width: auto;
5+
padding: 0 var(--space-quarter);
6+
text-align: left;
7+
}
8+
9+
.button img {
10+
width: 22px;
11+
height: 22px;
12+
}

0 commit comments

Comments
 (0)