diff --git a/src/entities/bank-card/model/operation-appearance.ts b/src/entities/bank-card/model/operation-appearance.ts index cf5ea23..16ac298 100644 --- a/src/entities/bank-card/model/operation-appearance.ts +++ b/src/entities/bank-card/model/operation-appearance.ts @@ -1,17 +1,20 @@ import type { BankOperation } from "../api/queries"; -type OperationIcon = { path: string; color: string }; +type OperationIcon = { path: string; color: string; animation?: string }; const categoryIcons: Record = { алкоголь: { - path: "M7 3h10v5a5 5 0 0 1-10 0V3Z M7 7h10 M12 13v8 M8 21h8", + animation: "wine", + path: "M7 3h10v5a5 5 0 0 1-10 0V3Z M7 7h10v1a5 5 0 0 1-10 0V7Z M12 13v8 M8 21h8", color: "grape", }, вклады: { - path: "M4 8c0-4 16-4 16 0s-16 4-16 0Z M4 8v5c0 4 16 4 16 0V8 M4 13v5c0 4 16 4 16 0v-5", + animation: "deposit", + path: "M6 9a6 6 0 1 0 12 0a6 6 0 1 0-12 0 M10.5 12V6h2a1.5 1.5 0 0 1 0 3h-3m0 1.5h3 M4 16c0 4 16 4 16 0 M4 19c0 4 16 4 16 0", color: "green", }, "инвестиции и накопления": { + animation: "growth", path: "M3 3v18h18 M6 15l5-5 4 3 6-8 M16 5h5v5", color: "green", }, @@ -19,9 +22,14 @@ const categoryIcons: Record = { path: "M2 8l10-5 10 5-10 5L2 8Z M6 10v7c4 3 8 3 12 0v-7 M22 8v9", color: "blue", }, - переводы: { path: "M3 7h18l-4-4 M21 17H3l4 4", color: "blue" }, + переводы: { + animation: "transfer", + path: "M3 7h18l-4-4 M21 17H3l4 4", + color: "blue", + }, "погашение кредита": { - path: "M3 5h18v14H3V5Z M3 9h18 M8 14l3 3 5-5", + animation: "repayment", + path: "M3 5h18v14H3V5Z M3 9h18 M10 18v-7h3a2 2 0 0 1 0 4H9m0 1.5h4", color: "orange", }, подписки: { @@ -33,6 +41,7 @@ const categoryIcons: Record = { color: "blue", }, "связь и интернет": { + animation: "wifi", path: "M3 7a14 14 0 0 1 18 0 M6 11a9 9 0 0 1 12 0 M9 15a4 4 0 0 1 6 0 M12 19h.01", color: "blue", }, @@ -53,6 +62,7 @@ const categoryIcons: Record = { color: "orange", }, "хостинг и ии-сервисы": { + animation: "server", path: "M3 3h18v7H3V3Z M3 14h18v7H3v-7Z M7 6.5h.01 M7 17.5h.01 M15 6.5h3 M15 17.5h3", color: "grape", }, @@ -61,11 +71,13 @@ const categoryIcons: Record = { color: "blue", }, продукты: { - path: "M3 9h18l-2 11H5L3 9Z M8 9l4-6 4 6 M9 13v3 M15 13v3", + animation: "basket", + path: "M3 9h18l-2 11H5L3 9Z M8 9l4-6 4 6 M9 13v3 M15 13v3 M10 6a2 2 0 1 0 4 0a2 2 0 1 0-4 0", color: "green", }, кафе: { - path: "M4 8h12v7a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8Z M16 9h2a3 3 0 0 1 0 6h-2 M8 3v2 M12 3v2 M3 22h15", + animation: "coffee", + path: "M4 8h12v7a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8Z M16 9h2a3 3 0 0 1 0 6h-2 M8 6c-2-1 2-2 0-4 M12 6c-2-1 2-2 0-4 M3 22h15", color: "orange", }, такси: { @@ -73,7 +85,8 @@ const categoryIcons: Record = { color: "yellow", }, транспорт: { - path: "M6 3h12a2 2 0 0 1 2 2v13H4V5a2 2 0 0 1 2-2Z M4 11h16 M7 15h1 M16 15h1 M7 18v3 M17 18v3 M9 6h6", + animation: "car", + path: "M4 16H2v-4l3-1 3-4h6l4 4 4 1v4h-2 M8 16h8 M6 11h12m-6-4v4 M4 16a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-2v4m-2-2h4 M16 16a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-2v4m-2-2h4", color: "blue", }, жилье: { path: "M3 11l9-8 9 8 M5 10v11h14V10 M9 21v-8h6v8", color: "orange" }, @@ -87,16 +100,25 @@ const categoryIcons: Record = { color: "grape", }, }; -const otherIcon = { +const otherIcon: OperationIcon = { + animation: "grid", path: "M4 4h6v6H4V4Z M14 4h6v6h-6V4Z M4 14h6v6H4v-6Z M14 14h6v6h-6v-6Z", color: "gray", }; -const operationIcons = { +const operationIcons: Record< + Exclude, + OperationIcon +> = { topup: { - path: "M20 8V5H5a2 2 0 0 1 0-4h13v4 M3 3v16a2 2 0 0 0 2 2h15V8H5 M16 12v5 M13.5 14.5h5", + animation: "topup", + path: "M20 8V5H5a2 2 0 0 1 0-4h13v4 M3 3v16a2 2 0 0 0 2 2h15V8H5 M16 12v5m-2.5-2.5h5", color: "green", }, - transfer: { path: "M3 7h18l-4-4 M21 17H3l4 4", color: "blue" }, + transfer: { + animation: "transfer", + path: "M3 7h18l-4-4 M21 17H3l4 4", + color: "blue", + }, refund: { path: "M8 4L3 9l5 5 M3 9h11a7 7 0 0 1 0 14", color: "teal" }, }; diff --git a/src/entities/bank-card/ui/BankOperationIcon.module.css b/src/entities/bank-card/ui/BankOperationIcon.module.css new file mode 100644 index 0000000..10e12ad --- /dev/null +++ b/src/entities/bank-card/ui/BankOperationIcon.module.css @@ -0,0 +1,365 @@ +.icon { + overflow: visible; + + & path { + --motion: none; + transform-box: fill-box; + transform-origin: center; + } +} + +.deposit path:nth-child(-n + 2) { + transform-box: view-box; + transform-origin: 12px 9px; + --motion: bank-operation-coin-turn 1000ms ease-in-out both; +} + +.repayment path:last-child { + --motion: bank-operation-ruble-in 1000ms ease-out both; +} + +.topup path:last-child { + --motion: bank-operation-plus-in 900ms ease-out both; +} + +.transfer path { + --shift: 3px; + --motion: bank-operation-exchange 700ms ease-in-out both; + &:last-child { + --shift: -3px; + } +} + +.growth { + & path:nth-child(2) { + stroke-dasharray: 1; + --motion: bank-operation-draw 450ms ease-out both; + } + & path:last-child { + --motion: bank-operation-arrow-rise 350ms ease-out 400ms both; + } +} + +.server { + & path:nth-child(3) { + --motion: bank-operation-blink 500ms ease-in-out both; + } + & path:nth-child(4) { + --motion: bank-operation-blink 500ms ease-in-out 220ms both; + } +} + +.basket { + & path:not(:last-child) { + --motion: bank-operation-settle 850ms ease-in-out both; + } + & path:last-child { + opacity: 0; + fill: currentColor; + --motion: bank-operation-grocery-drop 850ms ease-out both; + } +} + +.wine { + --motion: bank-operation-cheers 1100ms ease-in-out both; + transform-origin: 12px 21px; + & path:nth-child(2) { + fill: currentColor; + fill-opacity: 0.2; + transform-origin: bottom; + --motion: bank-operation-fill-glass 1100ms ease-out both; + } +} + +.coffee { + & path:nth-child(3), + & path:nth-child(4) { + --motion: bank-operation-steam 1000ms ease-out var(--delay, 0ms) both; + } + & path:nth-child(4) { + --delay: 180ms; + } +} + +.grid path { + --x: -1.5px; + --y: -1.5px; + --turn: 0deg; + --motion: bank-operation-rearrange 850ms ease-in-out both; + &:nth-child(2) { + --x: 1.5px; + --turn: 90deg; + } + &:nth-child(3) { + --y: 1.5px; + } + &:nth-child(4) { + --x: 1.5px; + --y: 1.5px; + } +} + +.wifi path { + --motion: bank-operation-signal 450ms ease-out var(--delay, 0ms) both; + &:nth-child(1) { + --delay: 360ms; + } + &:nth-child(2) { + --delay: 240ms; + } + &:nth-child(3) { + --delay: 120ms; + } +} + +.car { + --motion: bank-operation-drive 1100ms ease-in-out both; + & path:nth-child(-n + 3) { + --motion: bank-operation-suspension 275ms ease-in-out 4 both; + } + & path:nth-last-child(-n + 2) { + --motion: bank-operation-wheel-roll 1100ms ease-in-out both; + } +} + +@media (prefers-reduced-motion: no-preference) { + .icon[data-icon-animating] { + animation: var(--motion, none); + & path { + animation: var(--motion, none); + } + } +} + +@keyframes :global(bank-operation-coin-turn) { + 0%, + 100% { + transform: scaleX(1); + } + 35%, + 65% { + transform: scaleX(0.12); + } + 50% { + transform: scaleX(-1); + } +} + +@keyframes :global(bank-operation-settle) { + 0%, + 30%, + 100% { + transform: translateY(0); + } + 45% { + transform: translateY(1.5px); + } + 65% { + transform: translateY(-0.5px); + } +} + +@keyframes :global(bank-operation-ruble-in) { + 0% { + opacity: 0; + transform: translateY(-9px) scale(0.7); + } + 25% { + opacity: 1; + } + 65% { + transform: translateY(0) scale(1.15); + } + 80% { + transform: scale(0.95); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes :global(bank-operation-plus-in) { + 0% { + opacity: 0; + transform: translateY(-5px) scale(0.5); + } + 55% { + opacity: 1; + transform: translateY(0) scale(1.3); + } + 75% { + transform: scale(0.9); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes :global(bank-operation-exchange) { + 0%, + 100% { + transform: translateX(0); + } + 45% { + transform: translateX(var(--shift)); + } +} + +@keyframes :global(bank-operation-draw) { + from { + stroke-dashoffset: 1; + } + to { + stroke-dashoffset: 0; + } +} + +@keyframes :global(bank-operation-arrow-rise) { + 0%, + 100% { + transform: translateY(0); + } + 45% { + transform: translateY(-2px); + } +} + +@keyframes :global(bank-operation-blink) { + 0%, + 100% { + stroke-width: 1.6; + } + 45% { + stroke: var(--mantine-color-green-6); + stroke-width: 3; + } +} + +@keyframes :global(bank-operation-grocery-drop) { + 0% { + opacity: 0; + transform: translateY(-5px); + } + 20% { + opacity: 1; + } + 55% { + opacity: 1; + transform: translateY(7px); + } + 80%, + 100% { + opacity: 0; + transform: translateY(7px); + } +} + +@keyframes :global(bank-operation-fill-glass) { + 0% { + transform: scaleY(0.05); + } + 45%, + 75% { + transform: scaleY(1); + fill-opacity: 0.5; + } + 100% { + transform: scaleY(1); + fill-opacity: 0.2; + } +} + +@keyframes :global(bank-operation-cheers) { + 0%, + 40%, + 100% { + transform: rotate(0); + } + 65% { + transform: rotate(8deg); + } +} + +@keyframes :global(bank-operation-steam) { + 0% { + opacity: 0; + transform: translateY(2px); + } + 25% { + opacity: 1; + } + 70% { + opacity: 0; + transform: translateY(-3px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes :global(bank-operation-rearrange) { + 0%, + 100% { + transform: translate(0, 0) rotate(0); + } + 50% { + transform: translate(var(--x), var(--y)) rotate(var(--turn)); + } +} + +@keyframes :global(bank-operation-signal) { + 0% { + opacity: 0.1; + } + 100% { + opacity: 1; + } +} + +@keyframes :global(bank-operation-drive) { + 0% { + opacity: 0; + transform: translateX(-4px); + } + 20% { + opacity: 1; + } + 80% { + opacity: 1; + transform: translateX(3px); + } + 90% { + opacity: 0; + transform: translateX(4px); + } + 91% { + opacity: 0; + transform: translateX(0); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes :global(bank-operation-suspension) { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-0.5px); + } +} + +@keyframes :global(bank-operation-wheel-roll) { + from { + transform: rotate(0); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/entities/bank-card/ui/BankOperationIcon.tsx b/src/entities/bank-card/ui/BankOperationIcon.tsx index f2d952a..e7ad5bc 100644 --- a/src/entities/bank-card/ui/BankOperationIcon.tsx +++ b/src/entities/bank-card/ui/BankOperationIcon.tsx @@ -1,7 +1,9 @@ import type { FC } from "react"; +import { bindIconAnimation } from "@/shared/lib"; import type { BankOperation } from "../api/queries"; import { bankOperationAppearance } from "../model/operation-appearance"; +import classes from "./BankOperationIcon.module.css"; type BankOperationIconProps = { kind: BankOperation["kind"]; @@ -11,6 +13,8 @@ const BankOperationIcon: FC = (props) => { const icon = bankOperationAppearance(props.kind, props.categoryName); return ( = (props) => { focusable="false" style={{ flexShrink: 0, color: `var(--mantine-color-${icon.color}-7)` }} > - + {icon.path.split(" M").map((part, index) => ( + + ))} ); }; diff --git a/src/shared/lib/bind-icon-animation.test.tsx b/src/shared/lib/bind-icon-animation.test.tsx new file mode 100644 index 0000000..b3842d5 --- /dev/null +++ b/src/shared/lib/bind-icon-animation.test.tsx @@ -0,0 +1,87 @@ +import { fireEvent, render, waitFor } from "@testing-library/react"; +import { afterEach, expect, it, vi } from "vitest"; +import { bindIconAnimation } from "./bind-icon-animation"; + +afterEach(() => vi.restoreAllMocks()); + +it("finishes every delayed part after leaving and does not restart on quick re-entry", async () => { + vi.spyOn(window, "matchMedia").mockReturnValue({ + matches: true, + } as MediaQueryList); + const { container, getByRole } = render( + , + ); + const icon = container.querySelector("svg"); + if (!icon) throw new Error("Missing icon"); + const target = getByRole("button"); + let finishFirst!: () => void; + let finishLast!: () => void; + const getAnimations = vi.fn(() => [ + { + finished: new Promise((resolve) => { + finishFirst = resolve; + }), + }, + { + finished: new Promise((resolve) => { + finishLast = resolve; + }), + }, + ]); + Object.defineProperty(icon, "getAnimations", { value: getAnimations }); + fireEvent.mouseEnter(target); + fireEvent.mouseLeave(target); + expect(icon).toHaveAttribute("data-icon-animating"); + finishFirst(); + await Promise.resolve(); + expect(icon).toHaveAttribute("data-icon-animating"); + fireEvent.mouseEnter(target); + expect(getAnimations).toHaveBeenCalledTimes(1); + finishLast(); + await Promise.resolve(); + expect(icon).toHaveAttribute("data-icon-animating"); + fireEvent.mouseLeave(target); + await waitFor(() => expect(icon).not.toHaveAttribute("data-icon-animating")); + fireEvent.mouseEnter(target); + expect(getAnimations).toHaveBeenCalledTimes(2); +}); + +it("keeps playing after keyboard blur and releases cancelled animations", async () => { + const { container, getByRole, unmount } = render( + , + ); + const icon = container.querySelector("svg"); + if (!icon) throw new Error("Missing icon"); + const target = getByRole("button"); + // JSDOM does not model the browser keyboard-focus modality. + vi.spyOn(target, "matches").mockImplementation( + (selector) => selector === ":focus-visible", + ); + let cancel!: () => void; + Object.defineProperty(icon, "getAnimations", { + value: () => [ + { + finished: new Promise((_resolve, reject) => { + cancel = () => reject(new Error("cancelled")); + }), + }, + ], + }); + target.focus(); + expect(icon).toHaveAttribute("data-icon-animating"); + target.blur(); + expect(icon).toHaveAttribute("data-icon-animating"); + cancel(); + await waitFor(() => expect(icon).not.toHaveAttribute("data-icon-animating")); + target.focus(); + unmount(); + cancel(); + await Promise.resolve(); + expect(icon).not.toHaveAttribute("data-icon-animating"); +}); diff --git a/src/shared/lib/bind-icon-animation.ts b/src/shared/lib/bind-icon-animation.ts new file mode 100644 index 0000000..622f216 --- /dev/null +++ b/src/shared/lib/bind-icon-animation.ts @@ -0,0 +1,68 @@ +// React ref cleanup keeps native animation lifetimes tied to the mounted icon. +const bindIconAnimation = (icon: SVGSVGElement | null) => { + if (!icon) return; + const target = icon.closest("a, button") ?? icon.parentElement; + if (!target) return; + let hovered = false; + let focused = false; + let running = false; + let disposed = false; + + const release = () => { + if (!running && !hovered && !focused) { + icon.removeAttribute("data-icon-animating"); + } + }; + const start = () => { + if (icon.hasAttribute("data-icon-animating")) return; + icon.setAttribute("data-icon-animating", ""); + running = true; + // Includes staggered/delayed children and finite iterations, without timers. + const animations = icon.getAnimations({ subtree: true }); + void Promise.allSettled( + animations.map((animation) => animation.finished), + ).then(() => { + if (disposed) return; + running = false; + release(); + }); + }; + const enter = () => { + if (!window.matchMedia("(hover: hover)").matches) return; + hovered = true; + start(); + }; + const leave = () => { + hovered = false; + release(); + }; + const focus = () => { + focused = + target.matches(":focus-visible") || + !!target.querySelector(":focus-visible"); + if (focused) start(); + }; + const blur = (event: FocusEvent) => { + if ( + event.relatedTarget instanceof Node && + target.contains(event.relatedTarget) + ) + return; + focused = false; + release(); + }; + target.addEventListener("mouseenter", enter); + target.addEventListener("mouseleave", leave); + target.addEventListener("focusin", focus); + target.addEventListener("focusout", blur); + return () => { + disposed = true; + target.removeEventListener("mouseenter", enter); + target.removeEventListener("mouseleave", leave); + target.removeEventListener("focusin", focus); + target.removeEventListener("focusout", blur); + icon.removeAttribute("data-icon-animating"); + }; +}; + +export { bindIconAnimation }; diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index a05bfe4..5e99c07 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -1,3 +1,4 @@ +export { bindIconAnimation } from "./bind-icon-animation"; export { formatDate, formatDecimal, diff --git a/src/shared/ui/AppShell.module.css b/src/shared/ui/AppShell.module.css index 30767b4..d862861 100644 --- a/src/shared/ui/AppShell.module.css +++ b/src/shared/ui/AppShell.module.css @@ -18,56 +18,192 @@ flex-shrink: 0; } +.portfolioIcon { + color: var(--mantine-color-blue-7); +} + +.cardsIcon { + color: var(--mantine-color-grape-7); + + & svg { + overflow: visible; + } +} + +.rubles { + pointer-events: none; + color: var(--mantine-color-green-8); + stroke: currentColor; + + & path { + opacity: 0; + --flight-x: -7px; + --flight-rotation: -20deg; + --flight-delay: 0ms; + } + + & path:nth-child(2) { + --flight-x: 0px; + --flight-rotation: 8deg; + --flight-delay: 120ms; + } + + & path:nth-child(3) { + --flight-x: 7px; + --flight-rotation: 24deg; + --flight-delay: 240ms; + } +} + +.stocksIcon { + color: var(--mantine-color-green-8); +} + +.bondsIcon { + color: var(--mantine-color-orange-8); +} + +.settingsIcon { + color: var(--mantine-color-gray-7); +} + +.portfolioRuble { + stroke: var(--mantine-color-green-8); + opacity: 0; +} + +.stockArrow { + transform-origin: 21px 5px; +} + +.topSlider, +.bottomSlider { + fill: var(--mantine-color-body); +} + @media (prefers-reduced-motion: no-preference) { - .drawStroke { + .navLink { + transition: background-color 180ms ease-in-out; + } + + .drawStroke, + .couponLines path { stroke-dasharray: 1; } .topSlider { - --slider-offset: -3px; + --slider-offset: 3px; + --slider-delay: 0ms; } .bottomSlider { - --slider-offset: 3px; + --slider-offset: -3px; + --slider-delay: 160ms; } - .navLink:focus-visible { - .portfolioIcon { - animation: portfolio-sway 650ms ease-in-out; + .couponLines path:nth-child(2) { + --line-delay: 140ms; + } + + .couponLines path:nth-child(3) { + --line-delay: 280ms; + } + + svg[data-icon-animating] { + .rubles path { + animation: ruble-fly 800ms ease-out var(--flight-delay) both; + } + .portfolioHandle { + animation: handle-lift 1200ms ease-in-out; + } + .portfolioLid { + animation: lid-open 1200ms ease-in-out; + } + .portfolioRuble { + animation: portfolio-ruble-reveal 1200ms ease-in-out; + } + .stockArrow { + animation: arrow-pop 300ms ease-out 400ms both; + } + .couponLines path { + animation: stroke-draw 300ms ease-out var(--line-delay, 0ms) both; } .drawStroke { - animation: stroke-draw 650ms ease-in-out; + animation: stroke-draw 450ms ease-out; } :is(.topSlider, .bottomSlider) { - animation: slider-slide 650ms ease-in-out; - } - } - - @media (hover: hover) { - .navLink:hover { - .portfolioIcon { - animation: portfolio-sway 650ms ease-in-out; - } - .drawStroke { - animation: stroke-draw 650ms ease-in-out; - } - :is(.topSlider, .bottomSlider) { - animation: slider-slide 650ms ease-in-out; - } + animation: slider-slide 650ms ease-in-out var(--slider-delay) both; } } } -@keyframes portfolio-sway { +@keyframes ruble-fly { + 0% { + opacity: 0; + transform: translate(0, 4px); + } + 20%, + 50% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translate(var(--flight-x), -10px) rotate(var(--flight-rotation)); + } +} + +@keyframes handle-lift { 0%, 100% { - transform: rotate(0); + transform: translateY(0); } - 30% { - transform: translateY(-1px) rotate(-10deg); + 20% { + transform: translateY(-3px); } - 65% { - transform: rotate(7deg); + 40%, + 75% { + transform: translateY(-3px); + } +} + +@keyframes lid-open { + 0%, + 15%, + 100% { + transform: translateY(0); + } + 40%, + 75% { + transform: translateY(-3px); + } +} + +@keyframes portfolio-ruble-reveal { + 0%, + 25%, + 100% { + opacity: 0; + transform: translateY(2px); + } + 40%, + 75% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes arrow-pop { + 0% { + opacity: 0; + transform: scale(0.7); + } + 60% { + opacity: 1; + transform: scale(1.2); + } + 100% { + opacity: 1; + transform: scale(1); } } diff --git a/src/shared/ui/AppShell.tsx b/src/shared/ui/AppShell.tsx index b9f85a5..cc2e63f 100644 --- a/src/shared/ui/AppShell.tsx +++ b/src/shared/ui/AppShell.tsx @@ -2,6 +2,7 @@ import { AppShell as MantineAppShell, NavLink, Text } from "@mantine/core"; import type { FC, ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { Outlet, NavLink as RouterNavLink, useLocation } from "react-router"; +import { bindIconAnimation } from "../lib"; import classes from "./AppShell.module.css"; type AppShellProps = { @@ -18,52 +19,75 @@ const AppShell: FC = (props) => { label: t("navigation.accounts"), iconClassName: classes.portfolioIcon, icon: ( - + <> + + + + + ), }, { to: "/cards", label: t("navigation.cards"), - icon: , + iconClassName: classes.cardsIcon, + icon: ( + <> + + + + + + + + ), }, { to: "/stocks", label: t("navigation.stocks"), + iconClassName: classes.stocksIcon, icon: ( <> + ), }, { to: "/bonds", label: t("navigation.bonds"), + iconClassName: classes.bondsIcon, icon: ( <> - + + + + + ), }, { to: "/settings", label: t("navigation.settings"), + iconClassName: classes.settingsIcon, icon: ( <> - - + + + ), }, @@ -91,6 +115,7 @@ const AppShell: FC = (props) => { className={`${classes.navIcon} ${item.iconClassName ?? ""}`} >