refactor: colocate frontend component files
Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Successful in 5m10s
Conventional commits / commits (pull_request) Successful in 25s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 5m10s

This commit is contained in:
2026-09-23 08:34:58 +03:00
parent 85fcfe3d07
commit b987fd8e78
190 changed files with 131 additions and 108 deletions
+2 -2
View File
@@ -76,8 +76,8 @@ Add/Edit/Delete; restore/category SVG остаются локальными.
## Примеры
- Общая busy-область: существующий [LoadingSkeleton](../packages/ui/src/LoadingSkeleton.tsx), а не domain widget в shared.
- Account-income state views: [страница](../src/pages/account-income/ui/AccountIncomePage.tsx) и её flat `components/*.view.tsx`; новый page-local filter остаётся здесь.
- Общая busy-область: существующий [LoadingSkeleton](../packages/ui/src/LoadingSkeleton/LoadingSkeleton.tsx), а не domain widget в shared.
- Account-income state views: [страница](../src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.tsx) и её `components/`; новый page-local filter остаётся здесь. Комплекты файлов группируются по [UI convention](conventions/ui.md#размещение-файлов-компонента).
- Portfolio query: [entity public API](../src/entities/portfolio/index.ts); новая UI-сводка с бизнес-смыслом не становится shared только из-за нескольких экранов.
## Проверка
+1 -1
View File
@@ -26,7 +26,7 @@ Stories не достигают application API, auth, VM и query providers д
контракт: keyboard activation, доступность действия, local callback. Не повторяй
RTL suite и не тестируй внутренности Mantine. Существующий
[Button](../../src/shared/ui/Button.stories.tsx) показывает themed control
без production wrapper; [LoadingSkeleton](../../packages/ui/src/LoadingSkeleton.stories.tsx)
без production wrapper; [LoadingSkeleton](../../packages/ui/src/LoadingSkeleton/LoadingSkeleton.stories.tsx)
— значимые busy states.
`npm run typecheck`, `npm run deps:storybook`, `npm run build:catalog` проверяют
+1 -1
View File
@@ -45,7 +45,7 @@ Finance: loading/error/empty/loaded, cached snapshot при refetch failure, ret
отсутствие старых данных после session/connection switch. ErrorBoundary: test-only
throwing route, безопасная ссылка и reset после navigation.
Примеры: [AuthForm test](../../src/features/auth/ui/AuthForm.test.tsx),
Примеры: [AuthForm test](../../src/features/auth/ui/AuthForm/AuthForm.test.tsx),
[client test](../../src/shared/api/client.test.ts),
[usePortfolio test](../../src/entities/portfolio/api/usePortfolio.test.tsx).
Не копируй fixtures/сценарии целиком, если задача затрагивает один исход.
+29 -6
View File
@@ -12,7 +12,7 @@ Mantine component и общую тему. Для выпадающего выбо
Семантический HTML для структуры страницы остаётся уместным.
Icons ищи у существующего owner (например,
[BankOperationIcon](../../src/entities/bank-card/ui/BankOperationIcon.tsx));
[BankOperationIcon](../../src/entities/bank-card/ui/BankOperationIcon/BankOperationIcon.tsx));
статичные Add/Edit/Delete доступны в `@wealthledger/icons`. Их SVG декоративны
по умолчанию; имя action принадлежит кнопке. Standalone meaningful icon получает
`aria-hidden={false}`, `role="img"` и доступное имя. Category/kind mappings
@@ -56,11 +56,34 @@ retry callbacks, domain grouping и derived presentation data. View получа
image fallback) и formatter одного переданного значения. Группировка/доли
коллекции готовятся в VM. Перенос fetch во view ради меньшего container нарушает границу.
Отдельный state/section с самостоятельной responsibility выноси в flat
`components/*.view.tsx`. Небольшой tightly-coupled renderer оставляй рядом.
Не создавай folder/barrel/test/CSS для каждого view и не дроби по line count.
Пример: [AccountIncomePage](../../src/pages/account-income/ui/AccountIncomePage.tsx)
и [его VM](../../src/pages/account-income/ui/AccountIncomePage.vm.ts).
Отдельный state/section с самостоятельной responsibility выноси в
`components/` своего component. Небольшой tightly-coupled renderer оставляй
рядом; размер файла сам по себе не требует разделения.
Пример: [AccountIncomePage](../../src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.tsx)
и [его VM](../../src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts).
## Размещение файлов компонента
Правило действует в `src` и `packages`: компонент с сопутствующими файлами
(view, VM, types, CSS, tests, stories) размещай в папке `Component/`.
Сохраняй полные имена файлов: `Component.tsx`, `Component.view.tsx`,
`Component.test.tsx`. Создавай только нужные файлы; одиночный компонент
оставляй непосредственно в `ui`, `components` или source-каталоге пакета.
Внутренние части принадлежат папке родителя; для набора state/section views
используй её `components/`. Внутри него действует то же правило: отдельная
папка объединяет комплект файлов, одиночный view остаётся файлом.
Стили и хуки нескольких соседних компонентов остаются у ближайшего общего
владельца; не копируй их и не прячь внутрь одного потребителя.
Папка компонента не создаёт новую FSD-границу. Публичные exports остаются в
существующем `index.ts` slice/segment или entry пакета; внутри используй
прямые относительные imports без дополнительных barrels на каждый компонент.
При переносе обновляй imports, ссылки документации и проверки, зависящие от пути.
Пример: [BankCardForm](../../src/entities/bank-card/ui/BankCardForm/BankCardForm.tsx)
лежит рядом со своим view, тестом и story; общий `BankForms.module.css`
остаётся в `bank-card/ui`.
## Формы
+1 -1
View File
@@ -9,7 +9,7 @@ module.exports = {
severity: "error",
from: { path: "\\.stories\\.tsx$|^\\.storybook/preview\\.tsx$" },
to: {
path: "^src/(shared/api|features/auth|app/routes|app/ui)/|^src/(entities|features)/[^/]+/api/|\\.vm\\.ts$|^src/pages/[^/]+/ui/[^/]+Page\\.tsx$|node_modules/(@tanstack/react-query|openapi-fetch|openapi-react-query)/",
path: "^src/(shared/api|features/auth|app/routes|app/ui)/|^src/(entities|features)/[^/]+/api/|\\.vm\\.ts$|^src/pages/[^/]+/ui/.*Page\\.tsx$|node_modules/(@tanstack/react-query|openapi-fetch|openapi-react-query)/",
reachable: true,
},
},
@@ -1,7 +1,7 @@
import { MantineProvider } from "@mantine/core";
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { LoadingSkeleton } from "./LoadingSkeleton";
import { LoadingSkeleton } from "../LoadingSkeleton/LoadingSkeleton";
import { StateView } from "./StateView";
describe("StateView", () => {
+3 -3
View File
@@ -1,3 +1,3 @@
export { EntryCard } from "./EntryCard";
export { LoadingSkeleton } from "./LoadingSkeleton";
export { StateView } from "./StateView";
export { EntryCard } from "./EntryCard/EntryCard";
export { LoadingSkeleton } from "./LoadingSkeleton/LoadingSkeleton";
export { StateView } from "./StateView/StateView";
@@ -1,8 +1,8 @@
import type { FC } from "react";
import { lazy } from "react";
import { Navigate, Route, Routes } from "react-router";
import { LazyPage } from "./RouteFallback";
import { Protected, Public } from "./RouteGuards";
import { LazyPage } from "../RouteFallback";
import { Protected, Public } from "../RouteGuards";
const CardPage = lazy(async () => {
const { CardPage } = await import("@/pages/card");
@@ -65,7 +65,7 @@ const SettingsPage = lazy(async () => {
return { default: SettingsPage };
});
const ProtectedLayout = lazy(async () => {
const { ProtectedLayout } = await import("./ProtectedLayout");
const { ProtectedLayout } = await import("../ProtectedLayout");
return { default: ProtectedLayout };
});
+1 -1
View File
@@ -1,6 +1,6 @@
import type { FC } from "react";
import { AppBreadcrumbs, AppShell } from "@/app/ui";
import { ProtectedDocumentTitle } from "./DocumentTitle";
import { ProtectedDocumentTitle } from "./DocumentTitle/DocumentTitle";
const ProtectedLayout: FC = () => {
return (
+2 -2
View File
@@ -1,2 +1,2 @@
export { AppRoutes } from "./AppRoutes";
export { RouteErrorBoundary } from "./RouteErrorBoundary";
export { AppRoutes } from "./AppRoutes/AppRoutes";
export { RouteErrorBoundary } from "./RouteErrorBoundary/RouteErrorBoundary";
+2 -2
View File
@@ -1,2 +1,2 @@
export { AppBreadcrumbs } from "./AppBreadcrumbs";
export { AppShell } from "./AppShell";
export { AppBreadcrumbs } from "./AppBreadcrumbs/AppBreadcrumbs";
export { AppShell } from "./AppShell/AppShell";
+5 -5
View File
@@ -20,11 +20,11 @@ export {
} from "./model/form-validation";
export { bankOperationKinds } from "./model/operation-kinds";
export { bankPeriod } from "./model/period";
export { BankCardFace } from "./ui/BankCardFace";
export { BankCardForm } from "./ui/BankCardForm";
export { BankCardFace } from "./ui/BankCardFace/BankCardFace";
export { BankCardForm } from "./ui/BankCardForm/BankCardForm";
export { BankFeedback } from "./ui/BankFeedback";
export { BankOperationIcon } from "./ui/BankOperationIcon";
export { BankOperationIcon } from "./ui/BankOperationIcon/BankOperationIcon";
export { BankPeriodPicker } from "./ui/BankPeriodPicker";
export { BankSummaryView } from "./ui/BankSummary.view";
export { CategoryManager } from "./ui/CategoryManager";
export { BankSummaryView } from "./ui/BankSummary/BankSummary.view";
export { CategoryManager } from "./ui/CategoryManager/CategoryManager";
export { useBankFormFocus } from "./ui/useBankFormFocus";
@@ -1,7 +1,7 @@
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { formatMoney } from "@/shared/lib";
import type { BankCard } from "../api/queries";
import type { BankCard } from "../../api/queries";
import classes from "./BankCardFace.module.css";
type BankCardFaceProps = { card: BankCard };
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { FormProvider, useForm } from "react-hook-form";
import type { BankCardValues } from "../model/form-validation";
import type { BankCardValues } from "../../model/form-validation";
import { BankCardFields } from "./BankCardForm.view";
const meta = {
@@ -6,7 +6,7 @@ import { describe, expect, it, vi } from "vitest";
import { configureAuthClient } from "@/shared/api";
import { createTestQueryClient } from "@/test/query";
import { server } from "@/test/server";
import type { BankCard } from "../api/queries";
import type { BankCard } from "../../api/queries";
import { BankCardForm } from "./BankCardForm";
const card: BankCard = {
@@ -4,18 +4,18 @@ import { type FC, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { formatDate } from "@/shared/lib";
import { useBankMutations } from "../api/mutations";
import { type BankCard, useBankCard } from "../api/queries";
import { useBankMutations } from "../../api/mutations";
import { type BankCard, useBankCard } from "../../api/queries";
import {
type BankCardValues,
bankCardSchema,
bankMoneyValue,
bankToday,
} from "../model/form-validation";
} from "../../model/form-validation";
import { BankFeedback } from "../BankFeedback";
import classes from "../BankForms.module.css";
import { useBankFormFocus } from "../useBankFormFocus";
import { BankCardFields } from "./BankCardForm.view";
import { BankFeedback } from "./BankFeedback";
import classes from "./BankForms.module.css";
import { useBankFormFocus } from "./useBankFormFocus";
type BankCardFormProps = { card?: BankCard; onClose: () => void };
const BankCardForm: FC<BankCardFormProps> = (props) => {
@@ -8,7 +8,7 @@ import {
bankDateInputValue,
bankDateSchema,
bankToday,
} from "../model/form-validation";
} from "../../model/form-validation";
type BankCardFieldsProps = { archived: boolean };
const BankCardFields: FC<BankCardFieldsProps> = (props) => {
@@ -1,8 +1,8 @@
import type { FC } from "react";
import { bindIconAnimation } from "@/shared/lib";
import type { BankOperation } from "../api/queries";
import type { BankOperation } from "../../api/queries";
import { bankOperationAppearance } from "../model/operation-appearance";
import { bankOperationAppearance } from "../../model/operation-appearance";
import classes from "./BankOperationIcon.module.css";
type BankOperationIconProps = {
@@ -3,8 +3,8 @@ import { LoadingSkeleton } from "@wealthledger/ui";
import type { CSSProperties, FC } from "react";
import { useTranslation } from "react-i18next";
import { formatMoney } from "@/shared/lib";
import type { BankSummary } from "../api/queries";
import { BankOperationIcon } from "./BankOperationIcon";
import type { BankSummary } from "../../api/queries";
import { BankOperationIcon } from "../BankOperationIcon/BankOperationIcon";
import classes from "./BankSummary.module.css";
type BankSummaryViewProps = {
@@ -18,12 +18,12 @@ import {
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { validationMessage } from "@/shared/i18n";
import { useBankMutations } from "../api/mutations";
import { type ExpenseCategory, useBankCategories } from "../api/queries";
import { BankFeedback } from "./BankFeedback";
import classes from "./BankForms.module.css";
import { BankOperationIcon } from "./BankOperationIcon";
import { useBankFormFocus } from "./useBankFormFocus";
import { useBankMutations } from "../../api/mutations";
import { type ExpenseCategory, useBankCategories } from "../../api/queries";
import { BankFeedback } from "../BankFeedback";
import classes from "../BankForms.module.css";
import { BankOperationIcon } from "../BankOperationIcon/BankOperationIcon";
import { useBankFormFocus } from "../useBankFormFocus";
const MAX_CATEGORY_NAME_LENGTH = 80;
+1 -1
View File
@@ -8,4 +8,4 @@ export {
type BrokerageAccountCard,
brokerageAccountCards,
} from "./model/display";
export { BrokerageAccountList } from "./ui/BrokerageAccountList";
export { BrokerageAccountList } from "./ui/BrokerageAccountList/BrokerageAccountList";
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { MemoryRouter } from "react-router";
import type { BrokerageAccount } from "../api/useBrokerageAccounts";
import { brokerageAccountCards } from "../model/display";
import type { BrokerageAccount } from "../../api/useBrokerageAccounts";
import { brokerageAccountCards } from "../../model/display";
import { BrokerageAccountList } from "./BrokerageAccountList";
const syntheticAccount = {
@@ -2,7 +2,7 @@ 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 { brokerageAccountCards } from "../model/display";
import { brokerageAccountCards } from "../../model/display";
import { BrokerageAccountList } from "./BrokerageAccountList";
describe("BrokerageAccountList", () => {
@@ -3,8 +3,8 @@ import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { formatDate, formatMoney } from "@/shared/lib";
import type { BrokerageAccountCard } from "../model/display";
import { accountTypeKey } from "../model/display";
import type { BrokerageAccountCard } from "../../model/display";
import { accountTypeKey } from "../../model/display";
import classes from "./BrokerageAccountList.module.css";
type BrokerageAccountListProps = { accounts: BrokerageAccountCard[] };
+1 -1
View File
@@ -6,4 +6,4 @@ export { BrokerageEventCard } from "./ui/BrokerageEventCard";
export {
BrokerageEventsPreview,
type BrokerageEventsPreviewState,
} from "./ui/BrokerageEventsPreview";
} from "./ui/BrokerageEventsPreview/BrokerageEventsPreview";
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { MemoryRouter } from "react-router";
import type { BrokerageEvent } from "../api/useBrokerageEvents";
import type { BrokerageEvent } from "../../api/useBrokerageEvents";
import { BrokerageEventsPreview } from "./BrokerageEventsPreview";
const syntheticEvent = {
@@ -3,8 +3,8 @@ import { LoadingSkeleton } from "@wealthledger/ui";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import type { BrokerageEvent } from "../api/useBrokerageEvents";
import { BrokerageEventCard } from "./BrokerageEventCard";
import type { BrokerageEvent } from "../../api/useBrokerageEvents";
import { BrokerageEventCard } from "../BrokerageEventCard";
type BrokerageEventsPreviewProps = {
events: BrokerageEventsPreviewState;
+1 -1
View File
@@ -1,4 +1,4 @@
export { AuthProvider, useAuth } from "./model/auth";
export { AuthPageLayout } from "./ui/AuthPageLayout";
export { AuthPageLayout } from "./ui/AuthPageLayout/AuthPageLayout";
export { LoginForm } from "./ui/LoginForm";
export { RegisterForm } from "./ui/RegisterForm";
@@ -9,7 +9,7 @@ import {
type AuthFormValues,
loginSchema,
registerSchema,
} from "../model/validation";
} from "../../model/validation";
import classes from "./AuthForm.module.css";
type AuthFormProps = {
+1 -1
View File
@@ -1,7 +1,7 @@
import type { FC } from "react";
import { useLocation, useNavigate } from "react-router";
import { useAuth } from "../model/auth";
import { AuthForm } from "./AuthForm";
import { AuthForm } from "./AuthForm/AuthForm";
const LoginForm: FC = () => {
const { login } = useAuth();
+1 -1
View File
@@ -1,7 +1,7 @@
import type { FC } from "react";
import { useLocation, useNavigate } from "react-router";
import { useAuth } from "../model/auth";
import { AuthForm } from "./AuthForm";
import { AuthForm } from "./AuthForm/AuthForm";
const RegisterForm: FC = () => {
const { register } = useAuth();
+1 -1
View File
@@ -1 +1 @@
export { TbankConnectionSection } from "./ui/TbankConnectionSection";
export { TbankConnectionSection } from "./ui/TbankConnectionSection/TbankConnectionSection";
@@ -1,5 +1,5 @@
import { useAuthenticatedQuery } from "@/shared/api";
import { tbankConnectionQueryOptions } from "../model/connection-boundary";
import { tbankConnectionQueryOptions } from "../../model/connection-boundary";
function useTbankConnectionViewModel() {
const status = useAuthenticatedQuery({
@@ -15,7 +15,7 @@ import { apiErrorCode, apiQueryClient } from "@/shared/api";
import {
commitTbankConnectionBoundary,
type TbankConnectionStatus,
} from "../model/connection-boundary";
} from "../../model/connection-boundary";
type TbankConnectionSectionLoadedProps = {
status: TbankConnectionStatus;
+1 -1
View File
@@ -1 +1 @@
export { AccountEventsPage } from "./ui/AccountEventsPage";
export { AccountEventsPage } from "./ui/AccountEventsPage/AccountEventsPage";
+1 -1
View File
@@ -1 +1 @@
export { AccountIncomePage } from "./ui/AccountIncomePage";
export { AccountIncomePage } from "./ui/AccountIncomePage/AccountIncomePage";
@@ -23,7 +23,7 @@ import type { IncomeAnalytics } from "@/entities/income";
import { configureAuthClient } from "@/shared/api";
import { createQueryWrapper } from "@/test/query";
import { server } from "@/test/server";
import { isDate } from "../model/period";
import { isDate } from "../../model/period";
import { AccountIncomePage } from "./AccountIncomePage";
import { toIncomeDisplayModel } from "./AccountIncomePage.vm";
import { IncomePayments } from "./components/IncomePayments.view";
@@ -13,7 +13,7 @@ import {
isDate,
moscowDate,
parseDraftDate,
} from "../model/period";
} from "../../model/period";
const ISO_MONTH_LENGTH = 7;
@@ -15,7 +15,7 @@ import classes from "../AccountIncomePage.module.css";
import type { AccountIncomePageViewModel } from "../AccountIncomePage.vm";
import { IncomeComparisons } from "./IncomeComparisons.view";
import { IncomePayments } from "./IncomePayments.view";
import { IncomePeriodForm } from "./IncomePeriodForm.view";
import { IncomePeriodForm } from "./IncomePeriodForm/IncomePeriodForm.view";
import { IncomeSummary } from "./IncomeSummary.view";
type LoadedProps = Extract<AccountIncomePageViewModel, { state: "loaded" }>;
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { FC } from "react";
import { FormProvider, useForm, useFormState } from "react-hook-form";
import type { IncomeForm } from "../../model/period";
import type { IncomeForm } from "../../../../model/period";
import { IncomePeriodForm } from "./IncomePeriodForm.view";
const period = { from: "01.08.2026", to: "27.08.2026" };
@@ -8,8 +8,8 @@ import {
type IncomeForm,
moscowDate,
parseDraftDate,
} from "../../model/period";
import classes from "../AccountIncomePage.module.css";
} from "../../../../model/period";
import classes from "../../AccountIncomePage.module.css";
type IncomePeriodFormProps = {
onSubmit: FormEventHandler<HTMLFormElement>;
+1 -1
View File
@@ -1 +1 @@
export { AccountOperationsPage } from "./ui/AccountOperationsPage";
export { AccountOperationsPage } from "./ui/AccountOperationsPage/AccountOperationsPage";

Some files were not shown because too many files have changed in this diff Show More