refactor: enforce frontend view model boundaries
This commit is contained in:
@@ -17,6 +17,7 @@ const meta = {
|
||||
} satisfies Meta<typeof AddIcon>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Decorative: Story = {
|
||||
@@ -25,9 +26,11 @@ export const Decorative: Story = {
|
||||
<button aria-label="Добавить пример" type="button">
|
||||
<AddIcon size={20} strokeWidth={2} />
|
||||
</button>
|
||||
|
||||
<button aria-label="Изменить пример" type="button">
|
||||
<EditIcon size={18} />
|
||||
</button>
|
||||
|
||||
<button aria-label="Удалить пример" type="button">
|
||||
<DeleteIcon size={18} />
|
||||
</button>
|
||||
@@ -39,7 +42,9 @@ export const Meaningful: Story = {
|
||||
render: () => (
|
||||
<div style={{ color: "#2563eb", display: "flex", gap: 16 }}>
|
||||
<AddIcon aria-hidden={false} aria-label="Добавление" role="img" />
|
||||
|
||||
<EditIcon aria-hidden={false} aria-label="Редактирование" role="img" />
|
||||
|
||||
<DeleteIcon aria-hidden={false} aria-label="Удаление" role="img" />
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -12,6 +12,7 @@ describe("icon accessibility", () => {
|
||||
<Icon />
|
||||
</button>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("img")).not.toBeInTheDocument();
|
||||
expect(
|
||||
// eslint-disable-next-line testing-library/no-node-access -- This assertion checks DOM structure, decoration or focus behavior that semantic queries cannot establish.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { bindIconAnimation } from "../bind-icon-animation";
|
||||
import classes from "./OperationIcon.module.css";
|
||||
|
||||
const paths = {
|
||||
basket:
|
||||
"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",
|
||||
car:
|
||||
"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",
|
||||
cashWithdrawal: "M4 10V3h16v7 M7 7h10v14H7V7Z M7 11h10 M10 16h4",
|
||||
coffee:
|
||||
"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",
|
||||
deposit:
|
||||
"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",
|
||||
dice:
|
||||
"M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z M7 7h.01 M17 7h.01 M12 12h.01 M7 17h.01 M17 17h.01",
|
||||
download: "M3 3h18v13H3V3Z M8 21h8 M12 16v5 M12 6v7 M9 10l3 3 3-3",
|
||||
education: "M2 8l10-5 10 5-10 5L2 8Z M6 10v7c4 3 8 3 12 0v-7 M22 8v9",
|
||||
grid: "M4 4h6v6H4V4Z M14 4h6v6h-6V4Z M4 14h6v6H4v-6Z M14 14h6v6h-6v-6Z",
|
||||
growth: "M3 3v18h18 M6 15l5-5 4 3 6-8 M16 5h5v5",
|
||||
home: "M3 11l9-8 9 8 M5 10v11h14V10 M9 21v-8h6v8",
|
||||
medical: "M9 3h6v6h6v6h-6v6H9v-6H3V9h6V3Z",
|
||||
music: "M9 5v14a3 3 0 1 1-3-3h3 M9 5l11-2v14a3 3 0 1 1-3-3h3 M9 9l11-2",
|
||||
refund: "M8 4L3 9l5 5 M3 9h11a7 7 0 0 1 0 14",
|
||||
repayment: "M3 5h18v14H3V5Z M3 9h18 M10 18v-7h3a2 2 0 0 1 0 4H9m0 1.5h4",
|
||||
server:
|
||||
"M3 3h18v7H3V3Z M3 14h18v7H3v-7Z M7 6.5h.01 M7 17.5h.01 M15 6.5h3 M15 17.5h3",
|
||||
shield: "M12 2l8 4v6c0 5-8 10-8 10S4 17 4 12V6l8-4Z M8 12l3 3 5-6",
|
||||
shopping: "M5 7h14l2 14H3L5 7Z M8 8V6a4 4 0 0 1 8 0v2",
|
||||
smoking:
|
||||
"M3 14h18v5H3v-5Z M16 14v5 M8 10V8c0-2 3-2 3-4V2 M15 10V8c0-2 3-2 3-4",
|
||||
subscriptions:
|
||||
"M4 10a8 8 0 0 1 13-6l3 3 M20 3v4h-4 M20 14a8 8 0 0 1-13 6l-3-3 M4 21v-4h4",
|
||||
taxi:
|
||||
"M4 10l2-6h12l2 6 M3 10h18v8H3v-8Z M6 18v3 M18 18v3 M6 14h2 M16 14h2 M10 4V2h4v2",
|
||||
topup:
|
||||
"M20 8V5H5a2 2 0 0 1 0-4h13v4 M3 3v16a2 2 0 0 0 2 2h15V8H5 M16 12v5m-2.5-2.5h5",
|
||||
transfer: "M3 7h18l-4-4 M21 17H3l4 4",
|
||||
travel: "M10 3h4v7l7 5v3l-7-3v5l2 1v1l-4-1-4 1v-1l2-1v-5l-7 3v-3l7-5V3Z",
|
||||
wifi:
|
||||
"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",
|
||||
wine:
|
||||
"M7 3h10v5a5 5 0 0 1-10 0V3Z M7 7h10v1a5 5 0 0 1-10 0V7Z M12 13v8 M8 21h8",
|
||||
} as const;
|
||||
|
||||
type OperationIconProps = {
|
||||
animation?: string;
|
||||
color: string;
|
||||
variant: keyof typeof paths;
|
||||
};
|
||||
|
||||
const OperationIcon: FC<OperationIconProps> = (props) => {
|
||||
const { animation, color, variant } = props;
|
||||
// eslint-disable-next-line security/detect-object-injection -- Animation keys select CSS module classes only.
|
||||
const animationClass = animation ? classes[animation] : "";
|
||||
|
||||
// eslint-disable-next-line security/detect-object-injection -- Variant is a closed keyof the package geometry map.
|
||||
const parts = paths[variant]
|
||||
.split(" M")
|
||||
.map((part, index) => ({ d: index ? `M${part}` : part, key: part }));
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={`${classes.icon} ${animationClass}`}
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
ref={bindIconAnimation}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.6"
|
||||
style={{ color: `var(--mantine-color-${color}-7)`, flexShrink: 0 }}
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
>
|
||||
{parts.map(part => (
|
||||
<path d={part.d} key={part.key} pathLength={1} />
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { OperationIcon, type OperationIconProps };
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,91 @@
|
||||
import { fireEvent, render, screen, 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(globalThis, "matchMedia").mockReturnValue({
|
||||
matches: true,
|
||||
} as MediaQueryList);
|
||||
|
||||
const { container } = render(
|
||||
<button type="button">
|
||||
<svg aria-hidden="true" ref={bindIconAnimation} />
|
||||
Category
|
||||
</button>,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- Bind browser animation doubles to the decorative SVG, which has no accessible role.
|
||||
const icon = container.querySelector("svg");
|
||||
if (!icon) throw new Error("Missing icon");
|
||||
const target = screen.getByRole("button");
|
||||
let finishFirst!: () => void;
|
||||
let finishLast!: () => void;
|
||||
|
||||
const getAnimations = vi.fn(() => {
|
||||
const first = Promise.withResolvers<void>();
|
||||
const last = Promise.withResolvers<void>();
|
||||
finishFirst = first.resolve;
|
||||
finishLast = last.resolve;
|
||||
|
||||
return [{ finished: first.promise }, { finished: last.promise }];
|
||||
});
|
||||
|
||||
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, unmount } = render(
|
||||
<button type="button">
|
||||
<svg aria-hidden="true" ref={bindIconAnimation} />
|
||||
Category
|
||||
</button>,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- Bind browser animation doubles to the decorative SVG, which has no accessible role.
|
||||
const icon = container.querySelector("svg");
|
||||
if (!icon) throw new Error("Missing icon");
|
||||
const target = screen.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<void>((_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");
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
// React ref cleanup keeps native animation lifetimes tied to the mounted icon.
|
||||
const bindIconAnimation = (icon: null | SVGSVGElement) => {
|
||||
if (!icon) return;
|
||||
const target = icon.closest<HTMLElement>("a, button") ?? icon.parentElement;
|
||||
if (!target) return;
|
||||
let isHovered = false;
|
||||
let isFocused = false;
|
||||
let isRunning = false;
|
||||
let isDisposed = false;
|
||||
|
||||
const release = () => {
|
||||
if (!isRunning && !isHovered && !isFocused) {
|
||||
delete icon.dataset.iconAnimating;
|
||||
}
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (Object.hasOwn(icon.dataset, "iconAnimating")) return;
|
||||
icon.dataset.iconAnimating = "";
|
||||
isRunning = 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 (isDisposed) return;
|
||||
isRunning = false;
|
||||
release();
|
||||
});
|
||||
};
|
||||
|
||||
const enter = () => {
|
||||
if (!matchMedia("(hover: hover)").matches) return;
|
||||
isHovered = true;
|
||||
start();
|
||||
};
|
||||
|
||||
const leave = () => {
|
||||
isHovered = false;
|
||||
release();
|
||||
};
|
||||
|
||||
const focus = () => {
|
||||
isFocused
|
||||
= target.matches(":focus-visible")
|
||||
|| !!target.querySelector(":focus-visible");
|
||||
if (isFocused) start();
|
||||
};
|
||||
|
||||
const blur = (event: FocusEvent) => {
|
||||
if (
|
||||
event.relatedTarget instanceof Node
|
||||
&& target.contains(event.relatedTarget)
|
||||
)
|
||||
return;
|
||||
isFocused = false;
|
||||
release();
|
||||
};
|
||||
|
||||
target.addEventListener("mouseenter", enter);
|
||||
target.addEventListener("mouseleave", leave);
|
||||
target.addEventListener("focusin", focus);
|
||||
target.addEventListener("focusout", blur);
|
||||
|
||||
return () => {
|
||||
isDisposed = true;
|
||||
target.removeEventListener("mouseenter", enter);
|
||||
target.removeEventListener("mouseleave", leave);
|
||||
target.removeEventListener("focusin", focus);
|
||||
target.removeEventListener("focusout", blur);
|
||||
delete icon.dataset.iconAnimating;
|
||||
};
|
||||
};
|
||||
|
||||
export { bindIconAnimation };
|
||||
@@ -1,4 +1,17 @@
|
||||
export {
|
||||
OperationIcon,
|
||||
type OperationIconProps,
|
||||
} from "./OperationIcon/OperationIcon";
|
||||
export { AddIcon } from "./svg/AddIcon";
|
||||
export { BondsIcon } from "./svg/BondsIcon";
|
||||
export { CardsIcon } from "./svg/CardsIcon";
|
||||
export { CategoriesIcon } from "./svg/CategoriesIcon";
|
||||
export { ContactlessIcon } from "./svg/ContactlessIcon";
|
||||
export { DeleteIcon } from "./svg/DeleteIcon";
|
||||
export { EditIcon } from "./svg/EditIcon";
|
||||
export { PortfolioIcon } from "./svg/PortfolioIcon";
|
||||
export { RestoreIcon } from "./svg/RestoreIcon";
|
||||
export { SettingsIcon } from "./svg/SettingsIcon";
|
||||
export { StocksIcon } from "./svg/StocksIcon";
|
||||
export { WalletIcon } from "./svg/WalletIcon";
|
||||
export type { IconProps } from "./types";
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.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,
|
||||
.couponLines path {
|
||||
stroke-dasharray: 1;
|
||||
}
|
||||
|
||||
.topSlider {
|
||||
--slider-offset: 3px;
|
||||
--slider-delay: 0ms;
|
||||
}
|
||||
|
||||
.bottomSlider {
|
||||
--slider-offset: -3px;
|
||||
--slider-delay: 160ms;
|
||||
}
|
||||
|
||||
.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 450ms ease-out;
|
||||
}
|
||||
:is(.topSlider, .bottomSlider) {
|
||||
animation: slider-slide 650ms ease-in-out var(--slider-delay) both;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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: translateY(0);
|
||||
}
|
||||
20% {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes stroke-draw {
|
||||
0% {
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slider-slide {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(var(--slider-offset));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { IconProps } from "../types";
|
||||
|
||||
const AddIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import type { IconProps } from "../types";
|
||||
|
||||
import { bindIconAnimation } from "../bind-icon-animation";
|
||||
import classes from "../navigation.module.css";
|
||||
|
||||
const BondsIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size}
|
||||
ref={bindIconAnimation}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
{...svgProps}
|
||||
>
|
||||
<path d="M6 3h12v18l-3-2-3 2-3-2-3 2V3Z" />
|
||||
|
||||
<g className={classes.couponLines}>
|
||||
<path d="M9 7h6" pathLength={1} />
|
||||
|
||||
<path d="M9 11h6" pathLength={1} />
|
||||
|
||||
<path d="M9 15h3" pathLength={1} />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { BondsIcon };
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import type { IconProps } from "../types";
|
||||
|
||||
import { bindIconAnimation } from "../bind-icon-animation";
|
||||
import classes from "../navigation.module.css";
|
||||
|
||||
const CardsIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size}
|
||||
ref={bindIconAnimation}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
{...svgProps}
|
||||
>
|
||||
<path d="M3 5h18v14H3V5Zm0 4h18M6 15h4" />
|
||||
|
||||
<g className={classes.rubles}>
|
||||
<path d="M10 12V4h3a2 2 0 0 1 0 4H9m0 2h4" />
|
||||
|
||||
<path d="M10 12V4h3a2 2 0 0 1 0 4H9m0 2h4" />
|
||||
|
||||
<path d="M10 12V4h3a2 2 0 0 1 0 4H9m0 2h4" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { CardsIcon };
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import type { IconProps } from "../types";
|
||||
|
||||
const CategoriesIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
{...svgProps}
|
||||
>
|
||||
<path d="M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { CategoriesIcon };
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import type { IconProps } from "../types";
|
||||
|
||||
const ContactlessIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
{...svgProps}
|
||||
>
|
||||
<path
|
||||
d="M7 11a8 8 0 0 1 0 8m5-12a15 15 0 0 1 0 16m5-20a23 23 0 0 1 0 24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { ContactlessIcon };
|
||||
@@ -4,6 +4,7 @@ import type { IconProps } from "../types";
|
||||
|
||||
const DeleteIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { IconProps } from "../types";
|
||||
|
||||
const EditIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import type { IconProps } from "../types";
|
||||
|
||||
import { bindIconAnimation } from "../bind-icon-animation";
|
||||
import classes from "../navigation.module.css";
|
||||
|
||||
const PortfolioIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size}
|
||||
ref={bindIconAnimation}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
{...svgProps}
|
||||
>
|
||||
<path d="M3 10v9h18v-9" />
|
||||
|
||||
<path className={classes.portfolioHandle} d="M8 6V4h8v2" />
|
||||
|
||||
<path
|
||||
className={classes.portfolioLid}
|
||||
d="M3 10V8a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v2H3Zm6 0v3h6v-3"
|
||||
/>
|
||||
|
||||
<path
|
||||
className={classes.portfolioRuble}
|
||||
d="M10 18v-7h3a2 2 0 0 1 0 4H9m0 1.5h4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { PortfolioIcon };
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import type { IconProps } from "../types";
|
||||
|
||||
const RestoreIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
{...svgProps}
|
||||
>
|
||||
<path d="M3 3h18v4H3z M5 7v14h14V7 M12 17V10m-3 3 3-3 3 3" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { RestoreIcon };
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import type { IconProps } from "../types";
|
||||
|
||||
import { bindIconAnimation } from "../bind-icon-animation";
|
||||
import classes from "../navigation.module.css";
|
||||
|
||||
const SettingsIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size}
|
||||
ref={bindIconAnimation}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
{...svgProps}
|
||||
>
|
||||
<path d="M4 7h16M4 17h16" />
|
||||
|
||||
<path className={classes.topSlider} d="M10 4v6h4V4h-4Z" />
|
||||
|
||||
<path className={classes.bottomSlider} d="M10 14v6h4v-6h-4Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { SettingsIcon };
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import type { IconProps } from "../types";
|
||||
|
||||
import { bindIconAnimation } from "../bind-icon-animation";
|
||||
import classes from "../navigation.module.css";
|
||||
|
||||
const StocksIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size}
|
||||
ref={bindIconAnimation}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
{...svgProps}
|
||||
>
|
||||
<path d="M3 3v18h18" />
|
||||
|
||||
<path
|
||||
className={classes.drawStroke}
|
||||
d="M7 14l5-5 4 3 5-7"
|
||||
pathLength={1}
|
||||
/>
|
||||
|
||||
<path className={classes.stockArrow} d="M16 5h5v5" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { StocksIcon };
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import type { IconProps } from "../types";
|
||||
|
||||
const WalletIcon: FC<IconProps> = (props) => {
|
||||
const { size = "24", ...svgProps } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
{...svgProps}
|
||||
>
|
||||
<rect height="15" rx="3" width="18" x="3" y="5" />
|
||||
|
||||
<path d="M3 9h18M7 15h3M14 5V3H7a4 4 0 0 0-4 4" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { WalletIcon };
|
||||
Reference in New Issue
Block a user