chore: finish frontend ESLint migration #18
Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Failing after 37m50s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Failing after 42m10s
Conventional commits / commits (pull_request) Successful in 29s

Enable every plugin recommended preset and replace Biome with ESLint formatting.

Preserve API contracts and JSX whitespace; verify source, tests, builds and tooling.
This commit is contained in:
2026-09-28 07:31:28 +03:00
parent 851457e03c
commit 3eafad2657
253 changed files with 10059 additions and 8864 deletions
+15 -13
View File
@@ -1,27 +1,27 @@
import { Button } from "@mantine/core";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button } from "@mantine/core";
import { fn } from "storybook/test";
import { EntryCard } from "./EntryCard";
const meta = {
title: "Design System/Components/EntryCard",
component: EntryCard,
tags: ["autodocs"],
args: {
title:
"Учебная запись с очень длинным названием для проверки узкого экрана",
metadata: "22.09.2026 · Синтетические данные",
initial: "У",
imageSrc: "data:image/png;base64,invalid",
value: "−1 250,00 ₽",
valueLabel: "Тестовое значение",
tone: "negative",
actions: (
<Button size="compact-sm" onClick={fn()}>
<Button onClick={fn()} size="compact-sm">
Открыть пример
</Button>
),
imageSrc: "data:image/png;base64,invalid",
initial: "У",
metadata: "22.09.2026 · Синтетические данные",
title:
"Учебная запись с очень длинным названием для проверки узкого экрана",
tone: "negative",
value: "−1 250,00 ₽",
valueLabel: "Тестовое значение",
},
component: EntryCard,
parameters: {
docs: {
description: {
@@ -30,6 +30,8 @@ const meta = {
},
},
},
tags: ["autodocs"],
title: "Design System/Components/EntryCard",
} satisfies Meta<typeof EntryCard>;
export default meta;
+13 -12
View File
@@ -1,6 +1,7 @@
import { MantineProvider } from "@mantine/core";
import { fireEvent, render, screen } from "@testing-library/react";
import { assert, describe, expect, it } from "vitest";
import { EntryCard } from "./EntryCard";
describe("EntryCard", () => {
@@ -8,13 +9,13 @@ describe("EntryCard", () => {
render(
<MantineProvider>
<EntryCard
title="Пример"
metadata="Описание"
actions={0}
initial="У"
metadata="Описание"
title="Пример"
tone="neutral"
value="—"
valueLabel="Неизвестно"
tone="neutral"
actions={0}
/>
</MantineProvider>,
);
@@ -22,21 +23,21 @@ describe("EntryCard", () => {
});
it("keeps a failed image fallback until a new source is supplied", () => {
const props = {
title: "Пример",
metadata: "Описание",
imageSrc: "data:image/png;base64,invalid",
initial: "У",
metadata: "Описание",
title: "Пример",
tone: "neutral" as const,
value: "—",
valueLabel: "Неизвестно",
tone: "neutral" as const,
imageSrc: "data:image/png;base64,invalid",
};
const { container, rerender } = render(
const { rerender } = render(
<MantineProvider>
<EntryCard {...props} />
</MantineProvider>,
);
// Decorative image has no accessible name; trigger its actual failure event.
const image = container.querySelector("img");
const image = screen.queryByAltText("");
assert(image);
fireEvent.error(image);
rerender(
@@ -44,7 +45,7 @@ describe("EntryCard", () => {
<EntryCard {...props} title="Обновлённый пример" />
</MantineProvider>,
);
expect(container.querySelector("img")).toBeNull();
expect(screen.queryByAltText("")).not.toBeInTheDocument();
expect(screen.getByText("У")).toBeInTheDocument();
expect(screen.getByText("Обновлённый пример")).toBeInTheDocument();
rerender(
@@ -52,7 +53,7 @@ describe("EntryCard", () => {
<EntryCard {...props} imageSrc="/replacement-logo.png" />
</MantineProvider>,
);
expect(container.querySelector("img")).toHaveAttribute(
expect(screen.queryByAltText("")).toHaveAttribute(
"src",
"/replacement-logo.png",
);
+15 -13
View File
@@ -1,33 +1,35 @@
import { Avatar, Paper, Stack, Text } from "@mantine/core";
import type { FC, ReactNode } from "react";
import { Avatar, Paper, Stack, Text } from "@mantine/core";
import classes from "./EntryCard.module.css";
type EntryCardProps = {
title: string;
metadata: string;
details?: ReactNode;
actions?: ReactNode;
initial: string;
icon?: ReactNode;
color?: string;
imageSrc?: string | null;
details?: ReactNode;
icon?: ReactNode;
imageSrc?: null | string;
initial: string;
metadata: string;
title: string;
tone: "negative" | "neutral" | "positive";
value: string;
valueLabel: string;
tone: "positive" | "negative" | "neutral";
};
const EntryCard: FC<EntryCardProps> = (props) => {
return (
<Paper
className={classes.row}
withBorder
style={
props.color
? {
(props.color)
? ({
background: `color-mix(in srgb, var(--mantine-color-${props.color}-0) 45%, var(--wl-color-surface))`,
}
})
: undefined
}
withBorder
>
<Avatar
alt=""
@@ -51,7 +53,7 @@ const EntryCard: FC<EntryCardProps> = (props) => {
<Text className={classes[props.tone]} fw={700}>
{props.value}
</Text>
<div className={props.actions ? classes.valueFooter : undefined}>
<div className={(props.actions) ? (classes.valueFooter) : undefined}>
<Text
className={`${classes[props.tone]} ${props.actions ? classes.valueLabel : ""}`}
size="sm"
@@ -1,16 +1,16 @@
import { Paper, Skeleton } from "@mantine/core";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Paper, Skeleton } from "@mantine/core";
import { LoadingSkeleton } from "./LoadingSkeleton";
const meta = {
title: "Design System/Components/LoadingSkeleton",
component: LoadingSkeleton,
tags: ["autodocs"],
args: { label: "Загрузка содержимого" },
argTypes: {
label: { control: "text" },
children: { table: { disable: true } },
label: { control: "text" },
},
component: LoadingSkeleton,
parameters: {
docs: {
description: {
@@ -19,15 +19,17 @@ const meta = {
},
},
},
render: (args) => (
render: arguments_ => (
<Paper p="md" withBorder>
<LoadingSkeleton {...args}>
<LoadingSkeleton {...arguments_}>
<Skeleton h={24} w="60%" />
<Skeleton h={72} />
<Skeleton h={16} w="80%" />
</LoadingSkeleton>
</Paper>
),
tags: ["autodocs"],
title: "Design System/Components/LoadingSkeleton",
} satisfies Meta<typeof LoadingSkeleton>;
export default meta;
@@ -1,6 +1,7 @@
import { Stack } from "@mantine/core";
import type { FC, PropsWithChildren } from "react";
import { Stack } from "@mantine/core";
type LoadingSkeletonProps = PropsWithChildren<{
label: string;
}>;
+29 -25
View File
@@ -1,36 +1,36 @@
import { Button } from "@mantine/core";
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { ComponentProps } from "react";
import { Button } from "@mantine/core";
import { fn } from "storybook/test";
import { StateView } from "./StateView";
type StateStoryArgs = ComponentProps<typeof StateView> & {
withAction: boolean;
type StateStoryArguments = ComponentProps<typeof StateView> & {
onAction: () => void;
withAction: boolean;
};
const meta = {
title: "Design System/Components/StateView",
component: StateView,
tags: ["autodocs"],
args: {
title: "Нет учебных данных",
text: "Синтетическое пустое состояние. Пользовательские данные не загружаются.",
error: false,
withAction: false,
onAction: fn(),
text: "Синтетическое пустое состояние. Пользовательские данные не загружаются.",
title: "Нет учебных данных",
withAction: false,
},
argTypes: {
title: { control: "text" },
text: { control: "text" },
error: { control: "boolean" },
action: { table: { disable: true } },
error: { control: "boolean" },
onAction: { control: false },
text: { control: "text" },
title: { control: "text" },
withAction: {
control: "boolean",
description: "Показать необязательное действие StateView",
},
onAction: { control: false },
},
component: StateView,
parameters: {
docs: {
description: {
@@ -39,40 +39,44 @@ const meta = {
},
},
},
render: (args) => {
const { withAction, onAction, ...stateProps } = args;
render: (arguments_) => {
const { onAction, withAction, ...stateProps } = arguments_;
return (
<StateView
{...stateProps}
action={
withAction ? (
<Button onClick={onAction} variant="default">
{args.error ? "Повторить локально" : "Выполнить пример"}
</Button>
) : undefined
(withAction)
? ((
<Button onClick={onAction} variant="default">
{arguments_.error ? "Повторить локально" : "Выполнить пример"}
</Button>
))
: undefined
}
/>
);
},
} satisfies Meta<StateStoryArgs>;
tags: ["autodocs"],
title: "Design System/Components/StateView",
} satisfies Meta<StateStoryArguments>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Empty: Story = { name: "Пустое состояние" };
export const EmptyWithAction: Story = {
name: "Пустое состояние с действием",
args: { withAction: true },
name: "Пустое состояние с действием",
};
export const Failed: Story = {
name: "Ошибка",
args: {
error: true,
title: "Учебная ошибка",
text: "Демонстрация недоступных данных без вызова API.",
title: "Учебная ошибка",
},
name: "Ошибка",
};
export const ErrorWithAction: Story = {
name: "Ошибка с retry",
args: { ...Failed.args, withAction: true },
name: "Ошибка с retry",
};
@@ -1,6 +1,7 @@
import { MantineProvider } from "@mantine/core";
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { LoadingSkeleton } from "../LoadingSkeleton/LoadingSkeleton";
import { StateView } from "./StateView";
+6 -5
View File
@@ -1,19 +1,20 @@
import { Paper, Stack, Text, Title } from "@mantine/core";
import type { FC, ReactNode } from "react";
import { Paper, Stack, Text, Title } from "@mantine/core";
type StateViewProps = {
title: string;
text: string;
action?: ReactNode;
error?: boolean;
text: string;
title: string;
};
const StateView: FC<StateViewProps> = (props) => {
const { title, text, action, error = false } = props;
const { action, error = false, text, title } = props;
const role = error ? "alert" : undefined;
return (
<Paper p="xl" radius="md" shadow="xs" withBorder role={role}>
<Paper p="xl" radius="md" role={role} shadow="xs" withBorder>
<Stack align="flex-start" gap="sm">
<Title order={2}>{title}</Title>
<Text c="dimmed">{text}</Text>
+1
View File
@@ -1,6 +1,7 @@
import { DEFAULT_THEME, mergeMantineTheme } from "@mantine/core";
import source from "@wealthledger/tokens/tokens.json";
import { describe, expect, it } from "vitest";
import { colorRoles, cssVariablesResolver, theme } from "./theme";
describe("design theme adapter", () => {
+30 -28
View File
@@ -1,8 +1,8 @@
import "./styles.css";
import {
type CSSVariablesResolver,
colorsTuple,
createTheme,
type CSSVariablesResolver,
getThemeColor,
type MantinePrimaryShade,
Modal,
@@ -11,78 +11,80 @@ import {
} from "@mantine/core";
import designTokens from "@wealthledger/tokens/tokens.json";
const { color, typography, spacing, radius, shadows, breakpoints } =
designTokens;
const { breakpoints, color, radius, shadows, spacing, typography }
= designTokens;
const mapValues = <T, U>(values: Record<string, T>, convert: (value: T) => U) =>
Object.fromEntries(
Object.entries(values).map(([name, value]) => [name, convert(value)]),
);
const colorRoles = mapValues(color.roles, (role) =>
const colorRoles = mapValues(color.roles, role =>
role.shade === null ? role.color : `${role.color}.${role.shade}`,
);
const theme = createTheme({
respectReducedMotion: true,
black: color.base.black,
breakpoints: mapValues(
breakpoints.values,
value => `${value}${breakpoints.unit}`,
),
colors: mapValues(color.palettes, colorsTuple),
components: {
Modal: Modal.extend({
defaultProps: {
overlayProps: { blur: 3 },
transitionProps: {
transition: "fade-down",
duration: 220,
exitDuration: 180,
timingFunction: "ease-out",
transition: "fade-down",
},
},
}),
},
primaryColor: color.roles.primary.color,
primaryShade: color.primaryShade as MantinePrimaryShade,
defaultRadius: radius.default,
white: color.base.white,
black: color.base.black,
colors: mapValues(color.palettes, colorsTuple),
fontFamily: typography.fontFamily,
fontFamilyMonospace: typography.fontFamilyMonospace,
fontSizes: mapValues(typography.fontSizes, (value) => rem(value)),
fontSizes: mapValues(typography.fontSizes, value => rem(value)),
fontWeights: mapValues(typography.fontWeights, String),
lineHeights: mapValues(typography.lineHeights, String),
headings: {
fontFamily: typography.fontFamily,
fontWeight: String(typography.fontWeights.bold),
sizes: mapValues(typography.headings, (value) => ({
sizes: mapValues(typography.headings, value => ({
fontSize: rem(value.fontSize),
lineHeight: String(value.lineHeight),
})),
},
spacing: mapValues(spacing.values, (value) => rem(value)),
radius: mapValues(radius.values, (value) => rem(value)),
shadows: mapValues(shadows.values, (layers) =>
lineHeights: mapValues(typography.lineHeights, String),
primaryColor: color.roles.primary.color,
primaryShade: color.primaryShade as MantinePrimaryShade,
radius: mapValues(radius.values, value => rem(value)),
respectReducedMotion: true,
shadows: mapValues(shadows.values, layers =>
layers
.map(
(layer) =>
layer =>
`${rem(layer.offsetX)} ${rem(layer.offsetY)} ${rem(layer.blur)} ${rem(layer.spread)} ${rgba(color.base[layer.color as keyof typeof color.base], layer.opacity)}`,
)
.join(", "),
),
breakpoints: mapValues(
breakpoints.values,
(value) => `${value}${breakpoints.unit}`,
),
spacing: mapValues(spacing.values, value => rem(value)),
white: color.base.white,
});
const cssVariablesResolver: CSSVariablesResolver = (resolvedTheme) => ({
const cssVariablesResolver: CSSVariablesResolver = resolvedTheme => ({
dark: {},
light: {
"--mantine-color-dimmed": getThemeColor(colorRoles.muted, resolvedTheme),
},
variables: Object.fromEntries(
Object.entries(colorRoles).map(([role, reference]) => [
`--wl-color-${role}`,
getThemeColor(reference, resolvedTheme),
]),
),
light: {
"--mantine-color-dimmed": getThemeColor(colorRoles.muted, resolvedTheme),
},
dark: {},
});
export { colorRoles, cssVariablesResolver, designTokens, theme };
export { colorRoles, cssVariablesResolver, theme };
export { default as designTokens } from "@wealthledger/tokens/tokens.json";
+1 -1
View File
@@ -3,9 +3,9 @@ import { defineConfig } from "vite";
export default defineConfig({
build: {
lib: {
cssFileName: "styles",
entry: { index: "src/index.ts", theme: "src/theme.ts" },
formats: ["es"],
cssFileName: "styles",
},
rolldownOptions: {
external: /^(react(?:-dom)?|@mantine\/[^/]+|@wealthledger\/tokens)(\/|$)/,