diff --git a/index.html b/index.html
index e33e6a4..42dc759 100644
--- a/index.html
+++ b/index.html
@@ -5,7 +5,7 @@
-
+
WealthLedger
diff --git a/src/app/routes/AppRoutes/AppRoutes.test.tsx b/src/app/routes/AppRoutes/AppRoutes.test.tsx
index ca97244..7707151 100644
--- a/src/app/routes/AppRoutes/AppRoutes.test.tsx
+++ b/src/app/routes/AppRoutes/AppRoutes.test.tsx
@@ -232,7 +232,8 @@ describe("AppRoutes", () => {
}),
).toBeInTheDocument();
expect(
- screen.getByRole("navigation", { name: "WealthLedger" }),
- ).toBeInTheDocument();
+ screen.getAllByRole("navigation", { name: "Основная навигация" }),
+ ).toHaveLength(2);
+ expect(screen.queryByRole("banner")).not.toBeInTheDocument();
});
});
diff --git a/src/app/ui/AppShell/AppShell.module.css b/src/app/ui/AppShell/AppShell.module.css
index 2baaffc..6d463bf 100644
--- a/src/app/ui/AppShell/AppShell.module.css
+++ b/src/app/ui/AppShell/AppShell.module.css
@@ -1,12 +1,22 @@
+.root {
+ --bottom-nav-height: 56px;
+}
+
.content {
max-width: 1120px;
margin-inline: auto;
}
-.header {
+.brand {
align-items: center;
+ border-bottom: 1px solid var(--mantine-color-gray-3);
display: flex;
- padding: 0 var(--mantine-spacing-md);
+ min-height: 60px;
+ padding-inline: var(--mantine-spacing-sm);
+}
+
+.navList {
+ padding: var(--mantine-spacing-sm);
}
.bottomNav {
@@ -52,29 +62,85 @@
.bottomNav {
background: var(--mantine-color-white);
border-top: 1px solid var(--mantine-color-gray-3);
- bottom: 0;
- display: flex;
- justify-content: space-around;
- left: 0;
- padding: var(--mantine-spacing-xs) 2px;
- gap: 2px;
- font-size: 11px;
- & a {
- min-width: 0;
- padding: 8px 2px;
- text-align: center;
- }
+ display: grid;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ inset: auto 0 0;
+ min-height: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px));
+ padding-bottom: env(safe-area-inset-bottom, 0px);
+ padding-inline:
+ max(4px, env(safe-area-inset-left, 0px))
+ max(4px, env(safe-area-inset-right, 0px));
position: fixed;
- right: 0;
- z-index: 200;
+ z-index: var(--mantine-z-index-app);
+ }
+
+ .bottomNavItem {
+ align-items: center;
+ color: var(--mantine-color-gray-7);
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ justify-content: center;
+ min-height: var(--bottom-nav-height);
+ min-width: 0;
+ text-decoration: none;
+
+ &:active .iconPill {
+ background-color: var(--mantine-color-gray-1);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--mantine-primary-color-filled);
+ outline-offset: -2px;
+ }
+ }
+
+ @media (hover: hover) {
+ .bottomNavItem:hover .iconPill {
+ background-color: var(--mantine-color-gray-1);
+ }
+ }
+
+ .iconPill {
+ align-items: center;
+ border-radius: 14px;
+ display: inline-flex;
+ height: 28px;
+ justify-content: center;
+ width: 48px;
+ }
+
+ .bottomNavLabel {
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 16px;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ @media (prefers-reduced-motion: no-preference) {
+ .bottomNavItem,
+ .iconPill {
+ transition:
+ background-color 150ms ease-in-out,
+ color 150ms ease-in-out;
+ }
}
.active {
color: var(--mantine-primary-color-filled);
- font-weight: 600;
+
+ & .iconPill {
+ background-color: var(--mantine-primary-color-light);
+ }
}
.main {
- padding-bottom: 5rem;
+ padding-bottom: calc(
+ var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px) +
+ var(--mantine-spacing-md)
+ );
}
}
diff --git a/src/app/ui/AppShell/AppShell.test.tsx b/src/app/ui/AppShell/AppShell.test.tsx
new file mode 100644
index 0000000..307f2a0
--- /dev/null
+++ b/src/app/ui/AppShell/AppShell.test.tsx
@@ -0,0 +1,47 @@
+import { MantineProvider } from "@mantine/core";
+import { render, screen, within } from "@testing-library/react";
+import { MemoryRouter } from "react-router";
+import { describe, expect, it } from "vitest";
+
+import { AppShell } from "./AppShell";
+
+function renderShell(path: string) {
+ return render(
+
+
+
+
+ ,
+ );
+}
+
+describe("AppShell", () => {
+ it("has no header banner", () => {
+ renderShell("/cards");
+
+ expect(screen.queryByRole("banner")).not.toBeInTheDocument();
+ });
+
+ it("marks only the current bottom navigation item", () => {
+ renderShell("/cards");
+
+ const bottomNav = screen
+ .getAllByRole("navigation", { name: "Основная навигация" })
+ .at(-1);
+
+ if (!bottomNav) {
+ throw new Error("Bottom navigation is not rendered");
+ }
+
+ const links = within(bottomNav).getAllByRole("link");
+
+ expect(links).toHaveLength(5);
+ expect(within(bottomNav).getByRole("link", { name: "Карты" })).toHaveAttribute(
+ "aria-current",
+ "page",
+ );
+ expect(
+ within(bottomNav).getByRole("link", { name: "Счета" }),
+ ).not.toHaveAttribute("aria-current");
+ });
+});
diff --git a/src/app/ui/AppShell/AppShell.tsx b/src/app/ui/AppShell/AppShell.tsx
index b498395..aee1778 100644
--- a/src/app/ui/AppShell/AppShell.tsx
+++ b/src/app/ui/AppShell/AppShell.tsx
@@ -17,6 +17,12 @@ type AppShellProps = {
contentAboveOutlet?: ReactNode;
};
+function getBottomNavItemClass({ isActive }: { isActive: boolean }) {
+ return isActive
+ ? `${classes.bottomNavItem} ${classes.active}`
+ : classes.bottomNavItem;
+}
+
const AppShell: FC = (props) => {
const { contentAboveOutlet } = props;
const { t } = useTranslation();
@@ -24,31 +30,31 @@ const AppShell: FC = (props) => {
const items = [
{
- icon: ,
+ Icon: PortfolioIcon,
iconClassName: classes.portfolioIcon,
label: t("navigation.accounts"),
to: "/accounts",
},
{
- icon: ,
+ Icon: CardsIcon,
iconClassName: classes.cardsIcon,
label: t("navigation.cards"),
to: "/cards",
},
{
- icon: ,
+ Icon: StocksIcon,
iconClassName: classes.stocksIcon,
label: t("navigation.stocks"),
to: "/stocks",
},
{
- icon: ,
+ Icon: BondsIcon,
iconClassName: classes.bondsIcon,
label: t("navigation.bonds"),
to: "/bonds",
},
{
- icon: ,
+ Icon: SettingsIcon,
iconClassName: classes.settingsIcon,
label: t("navigation.settings"),
to: "/settings",
@@ -57,32 +63,32 @@ const AppShell: FC = (props) => {
return (
-
- {t("appName")}
-
+
+
+ {t("appName")}
+
-
- {items.map(item => (
-
- {item.icon}
-
- )}
- to={item.to}
- />
- ))}
+
+ {items.map(item => (
+
+
+
+ )}
+ to={item.to}
+ />
+ ))}
+
@@ -93,22 +99,23 @@ const AppShell: FC = (props) => {
-
);
diff --git a/src/shared/i18n/locales/ru.ts b/src/shared/i18n/locales/ru.ts
index caadfb6..ca5f855 100644
--- a/src/shared/i18n/locales/ru.ts
+++ b/src/shared/i18n/locales/ru.ts
@@ -450,6 +450,7 @@ const ru = {
accounts: "Счета",
bonds: "Облигации",
cards: "Карты",
+ primary: "Основная навигация",
settings: "Настройки",
stocks: "Акции",
},