diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png
index dd2abcee93d..66afa1a7866 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png
index 443581d98f5..e50ea3fe2f2 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png
index 465ea69cb5f..a29cc002d92 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png
index 900a67f119f..43a9bdd4c2b 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/page-models/reports-page.js b/packages/desktop-client/e2e/page-models/reports-page.js
index 5ebbc428454..1eb33f1fa3d 100644
--- a/packages/desktop-client/e2e/page-models/reports-page.js
+++ b/packages/desktop-client/e2e/page-models/reports-page.js
@@ -8,6 +8,16 @@ export class ReportsPage {
return this.pageContent.getByRole('link', { name: /^Net/ }).waitFor();
}
+ async goToNetWorthPage() {
+ await this.pageContent.getByRole('link', { name: /^Net/ }).click();
+ return new ReportsPage(this.page);
+ }
+
+ async goToCashFlowPage() {
+ await this.pageContent.getByRole('link', { name: /^Cash/ }).click();
+ return new ReportsPage(this.page);
+ }
+
async getAvailableReportList() {
return this.pageContent
.getByRole('link')
diff --git a/packages/desktop-client/e2e/reports.test.js b/packages/desktop-client/e2e/reports.test.js
index 6a0763223b6..54db8e5c48d 100644
--- a/packages/desktop-client/e2e/reports.test.js
+++ b/packages/desktop-client/e2e/reports.test.js
@@ -34,4 +34,14 @@ test.describe('Reports', () => {
expect(reports).toEqual(['Net Worth', 'Cash Flow']);
await expect(page).toHaveScreenshot(screenshotConfig(page));
});
+
+ test('loads net worth graph and checks visuals', async () => {
+ await reportsPage.goToNetWorthPage();
+ await expect(page).toHaveScreenshot(screenshotConfig(page));
+ });
+
+ test('loads cash flow graph and checks visuals', async () => {
+ await reportsPage.goToCashFlowPage();
+ await expect(page).toHaveScreenshot(screenshotConfig(page));
+ });
});
diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png
new file mode 100644
index 00000000000..ebc5ed1f828
Binary files /dev/null and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-1-chromium-linux.png
new file mode 100644
index 00000000000..7549e2cf27c
Binary files /dev/null and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-1-chromium-linux.png differ
diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json
index e2b86fe6b52..5bd8879c351 100644
--- a/packages/desktop-client/package.json
+++ b/packages/desktop-client/package.json
@@ -74,7 +74,7 @@
"watch": "cross-env BROWSER=none yarn start",
"build": "cross-env INLINE_RUNTIME_CHUNK=false craco build",
"build:browser": "cross-env ./bin/build-browser",
- "generate:icons": "rm src/icons/*/*.js; cd src/icons && svgr --expand-props start --ext js -d . .",
+ "generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --typescript --expand-props start -d . .",
"test": "craco test",
"e2e": "npx playwright test --browser=chromium",
"vrt": "cross-env VRT=true npx playwright test --browser=chromium"
diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx
index c3e82793c0b..58792c219c6 100644
--- a/packages/desktop-client/src/components/FinancesApp.tsx
+++ b/packages/desktop-client/src/components/FinancesApp.tsx
@@ -5,7 +5,6 @@ import {
Route,
Routes,
Navigate,
- NavLink,
useNavigate,
BrowserRouter,
useLocation,
@@ -21,12 +20,8 @@ import checkForUpdateNotification from 'loot-core/src/client/update-notification
import * as undo from 'loot-core/src/platform/client/undo';
import { useActions } from '../hooks/useActions';
-import Add from '../icons/v1/Add';
-import Cog from '../icons/v1/Cog';
-import PiggyBank from '../icons/v1/PiggyBank';
-import Wallet from '../icons/v1/Wallet';
import { useResponsive } from '../ResponsiveProvider';
-import { theme, styles } from '../style';
+import { theme } from '../style';
import { ExposeNavigate } from '../util/router-tools';
import { getIsOutdated, getLatestVersion } from '../util/versions';
@@ -35,11 +30,13 @@ import { BudgetMonthCountProvider } from './budget/BudgetMonthCountContext';
import View from './common/View';
import GlobalKeys from './GlobalKeys';
import { ManageRulesPage } from './ManageRulesPage';
+import MobileNavTabs from './mobile/MobileNavTabs';
import Modals from './Modals';
import Notifications from './Notifications';
import { ManagePayeesPage } from './payees/ManagePayeesPage';
import Reports from './reports';
import { NarrowAlternate, WideComponent } from './responsive';
+import ScrollProvider from './ScrollProvider';
import Settings from './settings';
import FloatableSidebar, { SidebarProvider } from './sidebar';
import Titlebar, { TitlebarProvider } from './Titlebar';
@@ -73,48 +70,6 @@ function WideNotSupported({ children, redirectTo = '/budget' }) {
return isNarrowWidth ? children : null;
}
-function NavTab({ icon: TabIcon, name, path }) {
- return (
- ({
- alignItems: 'center',
- color: isActive ? theme.mobileNavItemSelected : theme.mobileNavItem,
- display: 'flex',
- flexDirection: 'column',
- textDecoration: 'none',
- })}
- >
-
- {name}
-
- );
-}
-
-function MobileNavTabs() {
- const { isNarrowWidth } = useResponsive();
- return (
-
-
-
-
-
-
- );
-}
-
function RouterBehaviors({ getAccounts }) {
let navigate = useNavigate();
useEffect(() => {
@@ -305,7 +260,9 @@ export default function FinancesAppWithContext() {
- {app}
+
+ {app}
+
diff --git a/packages/desktop-client/src/components/GlobalKeys.tsx b/packages/desktop-client/src/components/GlobalKeys.tsx
index a75c0763509..cff3d38ae2f 100644
--- a/packages/desktop-client/src/components/GlobalKeys.tsx
+++ b/packages/desktop-client/src/components/GlobalKeys.tsx
@@ -1,8 +1,9 @@
import { useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
import * as Platform from 'loot-core/src/client/platform';
+import useNavigate from '../hooks/useNavigate';
+
export default function GlobalKeys() {
let navigate = useNavigate();
useEffect(() => {
diff --git a/packages/desktop-client/src/components/LoggedInUser.tsx b/packages/desktop-client/src/components/LoggedInUser.tsx
index f1775be3410..0459cc9062b 100644
--- a/packages/desktop-client/src/components/LoggedInUser.tsx
+++ b/packages/desktop-client/src/components/LoggedInUser.tsx
@@ -78,7 +78,7 @@ export default function LoggedInUser({
return (
>;
+};
+
+function ManageRulesContent({
+ isModal,
+ payeeId,
+ setLoading,
+}: ManageRulesContentProps) {
let [allRules, setAllRules] = useState(null);
let [rules, setRules] = useState(null);
let [filter, setFilter] = useState('');
@@ -191,7 +209,7 @@ function ManageRulesContent({ isModal, payeeId, setLoading }) {
}, []);
function onCreateRule() {
- let rule = {
+ let rule: RuleEntity = {
stage: null,
conditionsOp: 'and',
conditions: [
@@ -314,11 +332,17 @@ function ManageRulesContent({ isModal, payeeId, setLoading }) {
);
}
+type ManageRulesProps = {
+ isModal: boolean;
+ payeeId: string | null;
+ setLoading?: Dispatch>;
+};
+
export default function ManageRules({
isModal,
payeeId,
setLoading = () => {},
-}) {
+}: ManageRulesProps) {
return (
@@ -129,7 +129,7 @@ function Notification({
positive
? theme.noticeBorder
: error
- ? theme.altErrorAccent
+ ? theme.errorBorder
: theme.altWarningAccent
}`,
...styles.shadowLarge,
@@ -175,7 +175,7 @@ function Notification({
positive
? theme.noticeBorder
: error
- ? theme.altErrorAccent
+ ? theme.errorBorder
: theme.altWarningAccent
}`,
color: 'currentColor',
@@ -185,7 +185,7 @@ function Notification({
backgroundColor: positive
? theme.noticeBackground
: error
- ? theme.altErrorBackground
+ ? theme.errorBackground
: theme.altWarningBackground,
},
}}
diff --git a/packages/desktop-client/src/components/ScrollProvider.tsx b/packages/desktop-client/src/components/ScrollProvider.tsx
new file mode 100644
index 00000000000..06e59fb60e1
--- /dev/null
+++ b/packages/desktop-client/src/components/ScrollProvider.tsx
@@ -0,0 +1,54 @@
+import React, {
+ type ReactNode,
+ createContext,
+ useState,
+ useContext,
+ useEffect,
+} from 'react';
+
+import debounce from 'debounce';
+
+type IScrollContext = {
+ scrollY: number | undefined;
+ isBottomReached: boolean;
+};
+
+const ScrollContext = createContext(undefined);
+
+type ScrollProviderProps = {
+ children?: ReactNode;
+};
+
+export default function ScrollProvider({ children }: ScrollProviderProps) {
+ const [scrollY, setScrollY] = useState(undefined);
+ const [isBottomReached, setIsBottomReached] = useState(false);
+
+ useEffect(() => {
+ const listenToScroll = debounce(e => {
+ setScrollY(e.target?.scrollTop || 0);
+ setIsBottomReached(
+ e.target?.scrollHeight - e.target?.scrollTop <= e.target?.clientHeight,
+ );
+ }, 20);
+
+ window.addEventListener('scroll', listenToScroll, {
+ capture: true,
+ passive: true,
+ });
+ return () =>
+ window.removeEventListener('scroll', listenToScroll, {
+ capture: true,
+ });
+ }, []);
+
+ return (
+
+ );
+}
+
+export function useScroll(): IScrollContext {
+ return useContext(ScrollContext);
+}
diff --git a/packages/desktop-client/src/components/Titlebar.tsx b/packages/desktop-client/src/components/Titlebar.tsx
index 8b0e593a1e5..bab3123ba3f 100644
--- a/packages/desktop-client/src/components/Titlebar.tsx
+++ b/packages/desktop-client/src/components/Titlebar.tsx
@@ -7,7 +7,7 @@ import React, {
type ReactNode,
} from 'react';
import { useSelector } from 'react-redux';
-import { Routes, Route, useLocation, useNavigate } from 'react-router-dom';
+import { Routes, Route, useLocation } from 'react-router-dom';
import * as Platform from 'loot-core/src/client/platform';
import * as queries from 'loot-core/src/client/queries';
@@ -15,6 +15,7 @@ import { listen } from 'loot-core/src/platform/client/fetch';
import { useActions } from '../hooks/useActions';
import useFeatureFlag from '../hooks/useFeatureFlag';
+import useNavigate from '../hooks/useNavigate';
import ArrowLeft from '../icons/v1/ArrowLeft';
import AlertTriangle from '../icons/v2/AlertTriangle';
import SvgEye from '../icons/v2/Eye';
@@ -152,7 +153,7 @@ export function SyncButton({ style, isMobile = false }: SyncButtonProps) {
const mobileColor =
syncState === 'error'
- ? theme.alt4ErrorText
+ ? theme.errorText
: syncState === 'disabled' ||
syncState === 'offline' ||
syncState === 'local'
@@ -160,7 +161,7 @@ export function SyncButton({ style, isMobile = false }: SyncButtonProps) {
: style.color;
const desktopColor =
syncState === 'error'
- ? theme.alt2ErrorText
+ ? theme.errorTextDark
: syncState === 'disabled' ||
syncState === 'offline' ||
syncState === 'local'
diff --git a/packages/desktop-client/src/components/UpdateNotification.tsx b/packages/desktop-client/src/components/UpdateNotification.tsx
index 7e844e02999..95b4051adee 100644
--- a/packages/desktop-client/src/components/UpdateNotification.tsx
+++ b/packages/desktop-client/src/components/UpdateNotification.tsx
@@ -36,7 +36,7 @@ export default function UpdateNotification() {
bottom: 0,
right: 0,
margin: '15px 17px',
- backgroundColor: theme.altPageTextPositive,
+ backgroundColor: theme.pageTextPositive,
color: theme.tableBackground,
padding: '7px 10px',
borderRadius: 4,
diff --git a/packages/desktop-client/src/components/accounts/MobileAccount.js b/packages/desktop-client/src/components/accounts/MobileAccount.js
index 83a3f6c5b14..fcf3a487e41 100644
--- a/packages/desktop-client/src/components/accounts/MobileAccount.js
+++ b/packages/desktop-client/src/components/accounts/MobileAccount.js
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import { useParams, useNavigate } from 'react-router-dom';
+import { useParams } from 'react-router-dom';
import debounce from 'debounce';
import memoizeOne from 'memoize-one';
@@ -20,6 +20,7 @@ import {
} from 'loot-core/src/shared/transactions';
import useCategories from '../../hooks/useCategories';
+import useNavigate from '../../hooks/useNavigate';
import { useSetThemeColor } from '../../hooks/useSetThemeColor';
import { theme } from '../../style';
diff --git a/packages/desktop-client/src/components/accounts/MobileAccounts.js b/packages/desktop-client/src/components/accounts/MobileAccounts.js
index 573bc9040b0..09364b154e8 100644
--- a/packages/desktop-client/src/components/accounts/MobileAccounts.js
+++ b/packages/desktop-client/src/components/accounts/MobileAccounts.js
@@ -1,11 +1,11 @@
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
-import { useNavigate } from 'react-router-dom';
import * as queries from 'loot-core/src/client/queries';
import { useActions } from '../../hooks/useActions';
import useCategories from '../../hooks/useCategories';
+import useNavigate from '../../hooks/useNavigate';
import { useSetThemeColor } from '../../hooks/useSetThemeColor';
import { theme, styles } from '../../style';
import Button from '../common/Button';
@@ -23,7 +23,7 @@ function AccountHeader({ name, amount, style = {} }) {
flex: '1 0 auto',
flexDirection: 'row',
marginTop: 10,
- color: theme.altpageTextSubdued,
+ color: theme.pageTextLight,
...style,
}}
>
@@ -143,7 +143,7 @@ function EmptyMessage({ onAdd }) {
Add Account
-
+
In the future, you can add accounts using the add button in the header.
diff --git a/packages/desktop-client/src/components/alerts.tsx b/packages/desktop-client/src/components/alerts.tsx
index f7a67e4f5c1..464922f28af 100644
--- a/packages/desktop-client/src/components/alerts.tsx
+++ b/packages/desktop-client/src/components/alerts.tsx
@@ -93,8 +93,8 @@ export const Error = ({ style, children }: ScopedAlertProps) => {
return (
{children}
diff --git a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.js b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.js
index 9f2a8cd41ad..1f2cb6abdfa 100644
--- a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.js
+++ b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.js
@@ -186,7 +186,7 @@ function PayeeList({
style={{
fontSize: isNarrowWidth ? 'inherit' : 11,
padding: 5,
- color: theme.altpageTextSubdued,
+ color: theme.pageTextLight,
textAlign: 'center',
}}
>
diff --git a/packages/desktop-client/src/components/budget/BudgetTotals.js b/packages/desktop-client/src/components/budget/BudgetTotals.js
index 0f66ddc40e1..5f2fa3079ff 100644
--- a/packages/desktop-client/src/components/budget/BudgetTotals.js
+++ b/packages/desktop-client/src/components/budget/BudgetTotals.js
@@ -56,7 +56,7 @@ const BudgetTotals = memo(function BudgetTotals({
{menuOpen && (
diff --git a/packages/desktop-client/src/components/budget/MonthCountSelector.tsx b/packages/desktop-client/src/components/budget/MonthCountSelector.tsx
index d3baecf087b..25137630cc5 100644
--- a/packages/desktop-client/src/components/budget/MonthCountSelector.tsx
+++ b/packages/desktop-client/src/components/budget/MonthCountSelector.tsx
@@ -42,9 +42,7 @@ export function MonthCountSelector({
calendars.push(
= i ? theme.altpageTextSubdued : theme.altButtonBareText
- }
+ color={maxMonths >= i ? theme.pageTextLight : theme.altButtonBareText}
onClick={() => onChange(i)}
/>,
);
diff --git a/packages/desktop-client/src/components/budget/index.js b/packages/desktop-client/src/components/budget/index.js
index 68a67017f91..65f984d2a08 100644
--- a/packages/desktop-client/src/components/budget/index.js
+++ b/packages/desktop-client/src/components/budget/index.js
@@ -7,7 +7,7 @@ import React, {
useRef,
} from 'react';
import { useSelector } from 'react-redux';
-import { useLocation, useMatch, useNavigate } from 'react-router-dom';
+import { useLocation, useMatch } from 'react-router-dom';
import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
import { send, listen } from 'loot-core/src/platform/client/fetch';
@@ -26,6 +26,7 @@ import * as monthUtils from 'loot-core/src/shared/months';
import { useActions } from '../../hooks/useActions';
import useCategories from '../../hooks/useCategories';
import useFeatureFlag from '../../hooks/useFeatureFlag';
+import useNavigate from '../../hooks/useNavigate';
import { styles } from '../../style';
import View from '../common/View';
import { TitlebarContext } from '../Titlebar';
diff --git a/packages/desktop-client/src/components/budget/report/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/report/BudgetSummary.tsx
index 8df2bb776ff..816f2dc7633 100644
--- a/packages/desktop-client/src/components/budget/report/BudgetSummary.tsx
+++ b/packages/desktop-client/src/components/budget/report/BudgetSummary.tsx
@@ -282,7 +282,7 @@ function Saved({ projected, style }: SavedProps) {
color: projected
? theme.alt2WarningText
: isNegative
- ? theme.alt2ErrorText
+ ? theme.errorTextDark
: theme.altUpcomingText,
},
])}`}
@@ -407,7 +407,7 @@ export function BudgetSummary({ month }: BudgetSummaryProps) {
{menuOpen && (
diff --git a/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx
index 99e5f7621df..9959da029f5 100644
--- a/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx
+++ b/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx
@@ -190,7 +190,7 @@ function ToBudget({
borderBottom: '1px solid transparent',
':hover': {
borderColor: isNegative
- ? theme.errorText
+ ? theme.errorBorder
: theme.pageTextPositive,
},
},
diff --git a/packages/desktop-client/src/components/budget/util.js b/packages/desktop-client/src/components/budget/util.ts
similarity index 72%
rename from packages/desktop-client/src/components/budget/util.js
rename to packages/desktop-client/src/components/budget/util.ts
index db5f7e76895..f8c4af733a1 100644
--- a/packages/desktop-client/src/components/budget/util.js
+++ b/packages/desktop-client/src/components/budget/util.ts
@@ -1,6 +1,9 @@
+import { type CategoryGroupEntity } from 'loot-core/src/types/models';
+
import { styles, theme } from '../../style';
+import { type DropPosition } from '../sort';
-export function addToBeBudgetedGroup(groups) {
+export function addToBeBudgetedGroup(groups: CategoryGroupEntity[]) {
return [
{
id: 'to-be-budgeted',
@@ -11,20 +14,20 @@ export function addToBeBudgetedGroup(groups) {
];
}
-export function separateGroups(categoryGroups) {
+export function separateGroups(categoryGroups: CategoryGroupEntity[]) {
return [
categoryGroups.filter(g => !g.is_income),
categoryGroups.find(g => g.is_income),
];
}
-export function makeAmountGrey(value) {
+export function makeAmountGrey(value: number | string) {
return value === 0 || value === '0' || value === ''
? { color: theme.altMenuItemText }
: null;
}
-export function makeAmountStyle(value) {
+export function makeAmountStyle(value: number) {
const greyed = makeAmountGrey(value);
if (greyed) {
return greyed;
@@ -35,7 +38,7 @@ export function makeAmountStyle(value) {
}
}
-export function makeAmountFullStyle(value) {
+export function makeAmountFullStyle(value: number) {
return {
color:
value < 0
@@ -46,7 +49,11 @@ export function makeAmountFullStyle(value) {
};
}
-export function findSortDown(arr, pos, targetId) {
+export function findSortDown(
+ arr: CategoryGroupEntity[],
+ pos: DropPosition,
+ targetId: string,
+) {
if (pos === 'top') {
return { targetId };
} else {
@@ -66,7 +73,11 @@ export function findSortDown(arr, pos, targetId) {
}
}
-export function findSortUp(arr, pos, targetId) {
+export function findSortUp(
+ arr: CategoryGroupEntity[],
+ pos: DropPosition,
+ targetId: string,
+) {
if (pos === 'bottom') {
return { targetId };
} else {
diff --git a/packages/desktop-client/src/components/common/ButtonLink.tsx b/packages/desktop-client/src/components/common/ButtonLink.tsx
index dca78f6c2d4..1ab6215b61d 100644
--- a/packages/desktop-client/src/components/common/ButtonLink.tsx
+++ b/packages/desktop-client/src/components/common/ButtonLink.tsx
@@ -1,6 +1,7 @@
import React, { type ComponentProps } from 'react';
-import { useMatch, useNavigate } from 'react-router-dom';
+import { useMatch } from 'react-router-dom';
+import useNavigate from '../../hooks/useNavigate';
import { type CSSProperties } from '../../style';
import Button from './Button';
diff --git a/packages/desktop-client/src/components/common/Link.tsx b/packages/desktop-client/src/components/common/Link.tsx
index 19f7fc08ec0..17b146cf349 100644
--- a/packages/desktop-client/src/components/common/Link.tsx
+++ b/packages/desktop-client/src/components/common/Link.tsx
@@ -1,8 +1,9 @@
import React, { type ReactNode, type ComponentProps } from 'react';
-import { NavLink, useMatch, useNavigate } from 'react-router-dom';
+import { NavLink, useMatch } from 'react-router-dom';
import { css } from 'glamor';
+import useNavigate from '../../hooks/useNavigate';
import { type CSSProperties, styles } from '../../style';
import Button from './Button';
diff --git a/packages/desktop-client/src/components/common/Modal.tsx b/packages/desktop-client/src/components/common/Modal.tsx
index 2bab19bc7f5..a03bdd004b3 100644
--- a/packages/desktop-client/src/components/common/Modal.tsx
+++ b/packages/desktop-client/src/components/common/Modal.tsx
@@ -18,7 +18,7 @@ import Text from './Text';
import View from './View';
export type ModalProps = {
- title: string;
+ title?: string;
isCurrent?: boolean;
isHidden?: boolean;
children: ReactNode | (() => ReactNode);
diff --git a/packages/desktop-client/src/components/manager/ConfigServer.tsx b/packages/desktop-client/src/components/manager/ConfigServer.tsx
index 1331fa5676c..cda664eb51a 100644
--- a/packages/desktop-client/src/components/manager/ConfigServer.tsx
+++ b/packages/desktop-client/src/components/manager/ConfigServer.tsx
@@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
import {
isNonProductionEnvironment,
@@ -7,6 +6,7 @@ import {
} from 'loot-core/src/shared/environment';
import { useActions } from '../../hooks/useActions';
+import useNavigate from '../../hooks/useNavigate';
import { useSetThemeColor } from '../../hooks/useSetThemeColor';
import { theme } from '../../style';
import Button, { ButtonWithLoading } from '../common/Button';
diff --git a/packages/desktop-client/src/components/manager/WelcomeScreen.js b/packages/desktop-client/src/components/manager/WelcomeScreen.js
index 53125f8d2f2..4d34b235d67 100644
--- a/packages/desktop-client/src/components/manager/WelcomeScreen.js
+++ b/packages/desktop-client/src/components/manager/WelcomeScreen.js
@@ -48,7 +48,7 @@ export default function WelcomeScreen() {
to help you get your bearings, and check out the rest of the
documentation while you’re there to learn more about advanced topics.
-
+
Get started by importing an existing budget file from Actual or
another budgeting app, or start fresh with an empty budget. You can
always create or import another budget later.
diff --git a/packages/desktop-client/src/components/manager/subscribe/ChangePassword.tsx b/packages/desktop-client/src/components/manager/subscribe/ChangePassword.tsx
index 5ee9f149963..68adb49b8d4 100644
--- a/packages/desktop-client/src/components/manager/subscribe/ChangePassword.tsx
+++ b/packages/desktop-client/src/components/manager/subscribe/ChangePassword.tsx
@@ -1,8 +1,8 @@
import React, { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
import { send } from 'loot-core/src/platform/client/fetch';
+import useNavigate from '../../../hooks/useNavigate';
import { theme } from '../../../style';
import Button from '../../common/Button';
import Text from '../../common/Text';
diff --git a/packages/desktop-client/src/components/manager/subscribe/Error.tsx b/packages/desktop-client/src/components/manager/subscribe/Error.tsx
index 969a0856358..30429f89fd2 100644
--- a/packages/desktop-client/src/components/manager/subscribe/Error.tsx
+++ b/packages/desktop-client/src/components/manager/subscribe/Error.tsx
@@ -1,6 +1,7 @@
import React from 'react';
-import { useNavigate, useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
+import useNavigate from '../../../hooks/useNavigate';
import { theme } from '../../../style';
import Button from '../../common/Button';
import Text from '../../common/Text';
diff --git a/packages/desktop-client/src/components/manager/subscribe/common.tsx b/packages/desktop-client/src/components/manager/subscribe/common.tsx
index fdc940f1ba4..9cc46ca8a66 100644
--- a/packages/desktop-client/src/components/manager/subscribe/common.tsx
+++ b/packages/desktop-client/src/components/manager/subscribe/common.tsx
@@ -1,8 +1,9 @@
import React, { useEffect, useState } from 'react';
-import { useNavigate, useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
import { send } from 'loot-core/src/platform/client/fetch';
+import useNavigate from '../../../hooks/useNavigate';
import { theme } from '../../../style';
import { useSetServerURL } from '../../ServerContext';
diff --git a/packages/desktop-client/src/components/mobile/MobileNavTabs.tsx b/packages/desktop-client/src/components/mobile/MobileNavTabs.tsx
new file mode 100644
index 00000000000..afa3c89b73a
--- /dev/null
+++ b/packages/desktop-client/src/components/mobile/MobileNavTabs.tsx
@@ -0,0 +1,82 @@
+import React, { type ComponentType, useMemo } from 'react';
+import { NavLink } from 'react-router-dom';
+
+import usePrevious from '../../hooks/usePrevious';
+import Add from '../../icons/v1/Add';
+import Cog from '../../icons/v1/Cog';
+import PiggyBank from '../../icons/v1/PiggyBank';
+import Wallet from '../../icons/v1/Wallet';
+import { useResponsive } from '../../ResponsiveProvider';
+import { theme, styles } from '../../style';
+import { useScroll } from '../ScrollProvider';
+
+const height = 70;
+
+export default function MobileNavTabs() {
+ const { isNarrowWidth } = useResponsive();
+ const { scrollY, isBottomReached } = useScroll();
+ const previousScrollY = usePrevious(scrollY);
+
+ const isVisible = useMemo(
+ () =>
+ previousScrollY === undefined ||
+ (!isBottomReached && previousScrollY > scrollY) ||
+ previousScrollY < 0,
+ [scrollY],
+ );
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+type NavTabIconProps = {
+ width: number;
+ height: number;
+};
+
+type NavTabProps = {
+ name: string;
+ path: string;
+ icon: ComponentType;
+};
+
+function NavTab({ icon: TabIcon, name, path }: NavTabProps) {
+ return (
+ ({
+ ...styles.noTapHighlight,
+ alignItems: 'center',
+ color: isActive ? theme.mobileNavItemSelected : theme.mobileNavItem,
+ display: 'flex',
+ flexDirection: 'column',
+ textDecoration: 'none',
+ })}
+ >
+
+ {name}
+
+ );
+}
diff --git a/packages/desktop-client/src/components/modals/CreateLocalAccount.tsx b/packages/desktop-client/src/components/modals/CreateLocalAccount.tsx
index 349cc4a221d..44984c491de 100644
--- a/packages/desktop-client/src/components/modals/CreateLocalAccount.tsx
+++ b/packages/desktop-client/src/components/modals/CreateLocalAccount.tsx
@@ -1,9 +1,9 @@
import React, { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
import { toRelaxedNumber } from 'loot-core/src/shared/util';
import { type BoundActions } from '../../hooks/useActions';
+import useNavigate from '../../hooks/useNavigate';
import { theme } from '../../style';
import { type CommonModalProps } from '../../types/modals';
import Button from '../common/Button';
@@ -110,7 +110,7 @@ function CreateLocalAccount({ modalProps, actions }: CreateLocalAccountProps) {
style={{
textAlign: 'right',
fontSize: '0.7em',
- color: theme.altpageTextSubdued,
+ color: theme.pageTextLight,
marginTop: 3,
}}
>
diff --git a/packages/desktop-client/src/components/modals/EditRule.js b/packages/desktop-client/src/components/modals/EditRule.js
index 603bc30cb8b..296ece82af3 100644
--- a/packages/desktop-client/src/components/modals/EditRule.js
+++ b/packages/desktop-client/src/components/modals/EditRule.js
@@ -283,7 +283,7 @@ function ScheduleDescription({ id }) {
—
diff --git a/packages/desktop-client/src/components/modals/FixEncryptionKey.js b/packages/desktop-client/src/components/modals/FixEncryptionKey.tsx
similarity index 92%
rename from packages/desktop-client/src/components/modals/FixEncryptionKey.js
rename to packages/desktop-client/src/components/modals/FixEncryptionKey.tsx
index 57e52e37edf..d327898f928 100644
--- a/packages/desktop-client/src/components/modals/FixEncryptionKey.js
+++ b/packages/desktop-client/src/components/modals/FixEncryptionKey.tsx
@@ -1,9 +1,12 @@
import React, { useState } from 'react';
+import { type FinanceModals } from 'loot-core/src/client/state-types/modals';
import { send } from 'loot-core/src/platform/client/fetch';
import { getTestKeyError } from 'loot-core/src/shared/errors';
+import { type BoundActions } from '../../hooks/useActions';
import { theme } from '../../style';
+import { type CommonModalProps } from '../../types/modals';
import Button, { ButtonWithLoading } from '../common/Button';
import ExternalLink from '../common/ExternalLink';
import InitialFocus from '../common/InitialFocus';
@@ -13,11 +16,17 @@ import Paragraph from '../common/Paragraph';
import Text from '../common/Text';
import View from '../common/View';
+type FixEncryptionKeyProps = {
+ modalProps: CommonModalProps;
+ actions: BoundActions;
+ options: FinanceModals['fix-encryption-key'];
+};
+
export default function FixEncryptionKey({
modalProps,
actions,
options = {},
-}) {
+}: FixEncryptionKeyProps) {
let { hasExistingKey, cloudFileId, onSuccess } = options;
let [password, setPassword] = useState('');
diff --git a/packages/desktop-client/src/components/modals/PlaidExternalMsg.js b/packages/desktop-client/src/components/modals/PlaidExternalMsg.tsx
similarity index 93%
rename from packages/desktop-client/src/components/modals/PlaidExternalMsg.js
rename to packages/desktop-client/src/components/modals/PlaidExternalMsg.tsx
index a07ebc1abd7..1130d66d47a 100644
--- a/packages/desktop-client/src/components/modals/PlaidExternalMsg.js
+++ b/packages/desktop-client/src/components/modals/PlaidExternalMsg.tsx
@@ -2,6 +2,7 @@ import React, { useState, useRef } from 'react';
import AnimatedLoading from '../../icons/AnimatedLoading';
import { theme } from '../../style';
+import { type CommonModalProps } from '../../types/modals';
import { Error } from '../alerts';
import Button from '../common/Button';
import Modal, { ModalButtons } from '../common/Modal';
@@ -19,12 +20,19 @@ function renderError(error) {
);
}
+type PlainExternalMsgProps = {
+ modalProps: CommonModalProps;
+ onMoveExternal: () => Promise<{ error; data }>;
+ onSuccess: (data: unknown) => Promise;
+ onClose?: () => void;
+};
+
export default function PlaidExternalMsg({
modalProps,
onMoveExternal,
onSuccess,
onClose: originalOnClose,
-}) {
+}: PlainExternalMsgProps) {
let [waiting, setWaiting] = useState(null);
let [success, setSuccess] = useState(false);
let [error, setError] = useState(null);
diff --git a/packages/desktop-client/src/components/payees/ManagePayeesPage.js b/packages/desktop-client/src/components/payees/ManagePayeesPage.js
index f4d5e903187..2d707f39131 100644
--- a/packages/desktop-client/src/components/payees/ManagePayeesPage.js
+++ b/packages/desktop-client/src/components/payees/ManagePayeesPage.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { useLocation } from 'react-router';
+import { useLocation } from 'react-router-dom';
import { Page } from '../Page';
diff --git a/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx b/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx
index 51263a18a8e..f5739966f4e 100644
--- a/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx
@@ -35,7 +35,10 @@ function CashFlowGraph({ graphData, isConcise }: CashFlowGraphProps) {
}
>
-
+
}
labels={x => x.premadeLabel}
style={{
- data: { stroke: theme.altpageTextSubdued },
+ data: { stroke: theme.pageTextLight },
}}
/>
@@ -634,7 +634,7 @@ export default function ScheduleDetails({ modalProps, actions, id }) {
{state.isCustom && (
Promise | void;
type UseDraggableArgs = {
item: unknown;
@@ -64,7 +66,6 @@ export function useDraggable({
return { dragRef };
}
-type DropPosition = 'top' | 'bottom';
export type OnDropCallback = (
id: unknown,
@@ -129,7 +130,7 @@ export const DropHighlightPosContext: Context =
createContext(null);
type DropHighlightProps = {
- pos: 'top' | 'bottom';
+ pos: DropPosition;
offset?: {
top?: number;
bottom?: number;
diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.js b/packages/desktop-client/src/components/transactions/MobileTransaction.js
index 28f7fecce06..51bf4e58dfd 100644
--- a/packages/desktop-client/src/components/transactions/MobileTransaction.js
+++ b/packages/desktop-client/src/components/transactions/MobileTransaction.js
@@ -7,7 +7,7 @@ import React, {
useRef,
} from 'react';
import { useSelector } from 'react-redux';
-import { useNavigate, useParams, Link } from 'react-router-dom';
+import { useParams, Link } from 'react-router-dom';
import { useFocusRing } from '@react-aria/focus';
import { useListBox, useListBoxSection, useOption } from '@react-aria/listbox';
@@ -45,6 +45,7 @@ import {
import { useActions } from '../../hooks/useActions';
import useCategories from '../../hooks/useCategories';
+import useNavigate from '../../hooks/useNavigate';
import { useSetThemeColor } from '../../hooks/useSetThemeColor';
import SvgAdd from '../../icons/v1/Add';
import CheveronLeft from '../../icons/v1/CheveronLeft';
@@ -157,7 +158,7 @@ function Status({ status }) {
switch (status) {
case 'missed':
- color = theme.alt3ErrorText;
+ color = theme.errorText;
break;
case 'due':
color = theme.alt2WarningText;
@@ -986,7 +987,7 @@ class Transaction extends PureComponent {
let isReconciled = transaction.reconciled;
let textStyle = isPreview && {
fontStyle: 'italic',
- color: theme.altpageTextSubdued,
+ color: theme.pageTextLight,
};
return (
diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.js b/packages/desktop-client/src/components/transactions/TransactionsTable.js
index bc0a9f24e46..70208ceb7a8 100644
--- a/packages/desktop-client/src/components/transactions/TransactionsTable.js
+++ b/packages/desktop-client/src/components/transactions/TransactionsTable.js
@@ -421,11 +421,11 @@ function StatusCell({
: status === 'reconciled'
? theme.noticeTextLight
: status === 'missed'
- ? theme.alt5ErrorText
+ ? theme.errorText
: status === 'due'
? theme.alt3WarningText
: selected
- ? theme.altPageTextLink
+ ? theme.pageTextLinkLight
: theme.pageTextSubdued;
function onSelect() {
@@ -1076,7 +1076,7 @@ const Transaction = memo(function Transaction(props) {
style={{
color:
notes === 'missed'
- ? theme.alt5ErrorText
+ ? theme.errorText
: notes === 'due'
? theme.alt5WarningText
: selected
@@ -1084,7 +1084,7 @@ const Transaction = memo(function Transaction(props) {
: theme.altTableText,
backgroundColor:
notes === 'missed'
- ? theme.altErrorBackground
+ ? theme.errorBackground
: notes === 'due'
? theme.altWarningBackground
: selected
diff --git a/packages/desktop-client/src/components/util/DisplayId.js b/packages/desktop-client/src/components/util/DisplayId.tsx
similarity index 89%
rename from packages/desktop-client/src/components/util/DisplayId.js
rename to packages/desktop-client/src/components/util/DisplayId.tsx
index 3947f9f1e5d..0a66bf34f2f 100644
--- a/packages/desktop-client/src/components/util/DisplayId.js
+++ b/packages/desktop-client/src/components/util/DisplayId.tsx
@@ -6,11 +6,17 @@ import { CachedPayees } from 'loot-core/src/client/data-hooks/payees';
import { theme } from '../../style';
import Text from '../common/Text';
+type DisplayIdProps = {
+ type: 'accounts' | 'payees';
+ id: string;
+ noneColor?: string;
+};
+
export default function DisplayId({
type,
id,
noneColor = theme.pageTextSubdued,
-}) {
+}: DisplayIdProps) {
let DataComponent;
switch (type) {
diff --git a/packages/desktop-client/src/hooks/useNavigate.ts b/packages/desktop-client/src/hooks/useNavigate.ts
new file mode 100644
index 00000000000..21387e423b0
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useNavigate.ts
@@ -0,0 +1,51 @@
+import { useCallback } from 'react';
+import {
+ type Location,
+ type NavigateFunction,
+ type NavigateOptions,
+ type To,
+ useLocation,
+ useNavigate as useNavigateReactRouter,
+} from 'react-router-dom';
+
+export default function useNavigate(): NavigateFunction {
+ const location = useLocation();
+ const navigate = useNavigateReactRouter();
+ return useCallback(
+ (to: To | number, options: NavigateOptions = {}) => {
+ if (typeof to === 'number') {
+ navigate(to);
+ } else {
+ const optionsWithPrevLocation: NavigateOptions = {
+ replace:
+ options.replace || isSamePath(to, location) ? true : undefined,
+ ...options,
+ state: {
+ ...options?.state,
+ previousLocation: location,
+ },
+ };
+
+ let { previousLocation, ...previousOriginalState } =
+ location.state || {};
+
+ if (
+ previousLocation == null ||
+ !isSamePath(to, previousLocation) ||
+ JSON.stringify(options?.state || {}) !==
+ JSON.stringify(previousOriginalState)
+ ) {
+ navigate(to, optionsWithPrevLocation);
+ } else {
+ // `to` is the same as the previous location. Just go back.
+ navigate(-1);
+ }
+ }
+ },
+ [navigate, location],
+ );
+}
+
+function isSamePath(to: To, location: Location) {
+ return to === location.pathname + location.search + location.hash;
+}
diff --git a/packages/desktop-client/src/icons/AnimatedLoading.js b/packages/desktop-client/src/icons/AnimatedLoading.tsx
similarity index 73%
rename from packages/desktop-client/src/icons/AnimatedLoading.js
rename to packages/desktop-client/src/icons/AnimatedLoading.tsx
index 803c86f1c6f..a153265a405 100644
--- a/packages/desktop-client/src/icons/AnimatedLoading.js
+++ b/packages/desktop-client/src/icons/AnimatedLoading.tsx
@@ -1,15 +1,15 @@
-import React from 'react';
+import React, { type SVGProps } from 'react';
-import { css } from 'glamor';
+import { css, keyframes } from 'glamor';
import Loading from './Loading';
-const rotation = css.keyframes({
+const rotation = keyframes({
'0%': { transform: 'rotate(-90deg)' },
'100%': { transform: 'rotate(666deg)' },
});
-function AnimatedLoading(props) {
+function AnimatedLoading(props: SVGProps) {
return (
{
+const SvgLoading = (props: SVGProps) => {
let { color = 'currentColor' } = props;
let [gradientId] = useState('gradient-' + Math.random());
diff --git a/packages/desktop-client/src/icons/logo/Logo.js b/packages/desktop-client/src/icons/logo/Logo.tsx
similarity index 89%
rename from packages/desktop-client/src/icons/logo/Logo.js
rename to packages/desktop-client/src/icons/logo/Logo.tsx
index 32ab54a0676..60c4d53af4d 100644
--- a/packages/desktop-client/src/icons/logo/Logo.js
+++ b/packages/desktop-client/src/icons/logo/Logo.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
-const SvgLogo = props => (
+import type { SVGProps } from 'react';
+const SvgLogo = (props: SVGProps) => (