chore: обновить инфраструктуру Mantine и шаблоны

- добавлен алиас infra и обновлены импорты Mantine
- настроены тема, цветовая схема и CSS-переменные Mantine
- добавлен переключатель цветовой схемы на главный экран
- обновлены шаблоны бизнес- и infra-модулей под фабрики
- добавлена команда генерации Petstore API и исключён generated-код из Biome
- обновлены JSDoc-комментарии React-компонентов
This commit is contained in:
2026-05-07 22:13:26 +03:00
parent f2358da397
commit 7c0f597840
31 changed files with 402 additions and 51 deletions

View File

@@ -1,2 +1,5 @@
# Базовый URL приложения # Базовый URL приложения
NEXT_PUBLIC_APP_URL=http://localhost:3000 NEXT_PUBLIC_APP_URL=http://localhost:3000
# Базовый URL локального Swagger Petstore API
NEXT_PUBLIC_PET_STORE_API_URL=http://localhost:8080/api/v3

View File

@@ -1 +1,5 @@
export { {{name.pascalCase}}Business } from './{{name.kebabCase}}.business' export { {{name.camelCase}}Factory } from './{{name.kebabCase}}.factory';
export type { {{name.pascalCase}} } from './types/{{name.kebabCase}}.type';
export type { {{name.pascalCase}}Api } from './types/{{name.kebabCase}}-api.type';
export type { {{name.pascalCase}}Deps } from './types/{{name.kebabCase}}-deps.type';
export type { {{name.pascalCase}}Factory } from './types/{{name.kebabCase}}-factory.type';

View File

@@ -0,0 +1,4 @@
/**
* Публичный API бизнес-модуля {{name.pascalCase}}.
*/
export type {{name.pascalCase}}Api = Record<string, never>;

View File

@@ -0,0 +1,4 @@
/**
* Зависимости бизнес-модуля {{name.pascalCase}}.
*/
export type {{name.pascalCase}}Deps = Record<string, never>;

View File

@@ -0,0 +1,7 @@
import type { {{name.pascalCase}}Api } from './{{name.kebabCase}}-api.type';
import type { {{name.pascalCase}}Deps } from './{{name.kebabCase}}-deps.type';
/**
* Фабрика публичного API бизнес-модуля {{name.pascalCase}}.
*/
export type {{name.pascalCase}}Factory = (deps: {{name.pascalCase}}Deps) => {{name.pascalCase}}Api;

View File

@@ -1,11 +1,4 @@
import type { HTMLAttributes } from 'react'
/** /**
* Параметры бизнес-модуля {{name.pascalCase}}. * Доменная сущность {{name.pascalCase}}.
*/ */
export type {{name.pascalCase}}BusinessParams = {} export type {{name.pascalCase}} = Record<string, never>;
/** HTML-атрибуты корневого элемента. */
type RootAttrs = HTMLAttributes<HTMLDivElement>
export type {{name.pascalCase}}BusinessProps = RootAttrs & {{name.pascalCase}}BusinessParams

View File

@@ -1,20 +0,0 @@
import cl from 'clsx'
import type { {{name.pascalCase}}BusinessProps } from './types/{{name.kebabCase}}.type'
import styles from './styles/{{name.kebabCase}}.module.css'
/**
* <Назначение бизнес-модуля {{name.pascalCase}} в 1 строке>.
*
* Используется для:
* - <сценарий 1>
* - <сценарий 2>
*/
export const {{name.pascalCase}}Business = (props: {{name.pascalCase}}BusinessProps) => {
const { children, className, ...htmlAttr } = props
return (
<div {...htmlAttr} className={cl(styles.root, className)}>
{children}
</div>
)
}

View File

@@ -0,0 +1,8 @@
import type { {{name.pascalCase}}Factory } from './types/{{name.kebabCase}}-factory.type';
/**
* Создаёт публичный API бизнес-модуля {{name.pascalCase}}.
*/
export const {{name.camelCase}}Factory: {{name.pascalCase}}Factory = () => {
return {};
};

View File

@@ -13,7 +13,8 @@
"!.next", "!.next",
"!dist", "!dist",
"!build", "!build",
"!.templates" "!.templates",
"!src/infrastructure/**/generated"
] ]
}, },
"formatter": { "formatter": {

View File

@@ -9,6 +9,7 @@
"lint": "biome check", "lint": "biome check",
"format": "biome format --write", "format": "biome format --write",
"sprite": "svg-sprites", "sprite": "svg-sprites",
"codegen:pet-store-api": "npx @gromlab/api-codegen@latest -i http://localhost:8080/api/v3/openapi.json -o src/infrastructure/pet-store-api/generated -n pet-store-api.generated",
"predev": "svg-sprites", "predev": "svg-sprites",
"prebuild": "svg-sprites" "prebuild": "svg-sprites"
}, },

View File

@@ -1,8 +1,11 @@
import '@mantine/core/styles.css'; import '@mantine/core/styles.css';
import 'shared/styles/global.css'; import 'shared/styles/global.css';
import { ColorSchemeScript } from '@mantine/core'; import {
import { MantineProvider } from 'infrastructure/mantine'; MantineColorSchemeScript,
MantineProvider,
mantineHtmlProps,
} from 'infra/mantine';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
@@ -31,11 +34,18 @@ export const metadata: Metadata = {
}, },
}; };
/**
* Корневой layout приложения Next.js.
*
* Используется для:
* - подключения провайдеров приложения
* - установки базовой HTML-структуры всех страниц
*/
export default function RootLayout({ children }: PropsWithChildren) { export default function RootLayout({ children }: PropsWithChildren) {
return ( return (
<html lang="ru" suppressHydrationWarning> <html lang="ru" {...mantineHtmlProps}>
<head> <head>
<ColorSchemeScript /> <MantineColorSchemeScript />
<link rel="preload" href="/sprites/icons.sprite.svg" as="image" /> <link rel="preload" href="/sprites/icons.sprite.svg" as="image" />
</head> </head>
<body> <body>

View File

@@ -6,6 +6,13 @@ export const metadata: Metadata = {
description: 'Главная страница приложения', description: 'Главная страница приложения',
}; };
/**
* Страница главного маршрута приложения.
*
* Используется для:
* - отображения главного экрана в корневом маршруте
* - композиции контента главной страницы
*/
export default function HomePage() { export default function HomePage() {
return <HomeScreen />; return <HomeScreen />;
} }

View File

@@ -0,0 +1,4 @@
import type { MantineColorScheme } from '@mantine/core';
export const MANTINE_COLOR_SCHEME_STORAGE_KEY = 'app-color-scheme';
export const MANTINE_DEFAULT_COLOR_SCHEME: MantineColorScheme = 'auto';

View File

@@ -0,0 +1,12 @@
export const darkTheme = {
primary: '#60a5fa',
background: '#0f172a',
backgroundHover: '#1e293b',
surface: '#111827',
text: '#f8fafc',
textSecondary: '#cbd5e1',
border: '#334155',
error: '#ff6b6b',
success: '#69db7c',
warning: '#ffd43b',
} as const;

View File

@@ -0,0 +1,12 @@
export const lightTheme = {
primary: '#2563eb',
background: '#ffffff',
backgroundHover: '#f8fafc',
surface: '#ffffff',
text: '#111827',
textSecondary: '#64748b',
border: '#e2e8f0',
error: '#e03131',
success: '#2f9e44',
warning: '#e67700',
} as const;

View File

@@ -0,0 +1,52 @@
import type { CSSVariablesResolver } from '@mantine/core';
import { darkTheme } from './dark-theme.config';
import { lightTheme } from './light-theme.config';
type ThemePalette = {
primary: string;
background: string;
backgroundHover: string;
surface: string;
text: string;
textSecondary: string;
border: string;
error: string;
success: string;
warning: string;
};
type ThemeCssVariables = {
'--color-primary': string;
'--color-bg': string;
'--color-bg-hover': string;
'--color-surface': string;
'--color-text': string;
'--color-text-secondary': string;
'--color-border': string;
'--color-error': string;
'--color-success': string;
'--color-warning': string;
};
const getThemeCssVariables = (theme: ThemePalette): ThemeCssVariables => {
return {
'--color-primary': theme.primary,
'--color-bg': theme.background,
'--color-bg-hover': theme.backgroundHover,
'--color-surface': theme.surface,
'--color-text': theme.text,
'--color-text-secondary': theme.textSecondary,
'--color-border': theme.border,
'--color-error': theme.error,
'--color-success': theme.success,
'--color-warning': theme.warning,
};
};
export const mantineCssVariablesResolver: CSSVariablesResolver = () => {
return {
variables: {},
light: getThemeCssVariables(lightTheme),
dark: getThemeCssVariables(darkTheme),
};
};

View File

@@ -0,0 +1,56 @@
import type { MantineColorsTuple, MantineThemeOverride } from '@mantine/core';
import { createTheme, virtualColor } from '@mantine/core';
import { darkTheme } from './dark-theme.config';
import { lightTheme } from './light-theme.config';
const brandLightColors = [
'#eff6ff',
'#dbeafe',
'#bfdbfe',
'#93c5fd',
'#60a5fa',
'#3b82f6',
'#2563eb',
'#1d4ed8',
'#1e40af',
'#1e3a8a',
] satisfies MantineColorsTuple;
const brandDarkColors = [
'#dbeafe',
'#bfdbfe',
'#93c5fd',
'#60a5fa',
'#3b82f6',
'#2563eb',
'#1d4ed8',
'#1e40af',
'#1e3a8a',
'#172554',
] satisfies MantineColorsTuple;
export const mantineTheme: MantineThemeOverride = createTheme({
black: darkTheme.background,
white: lightTheme.background,
primaryColor: 'brand',
primaryShade: { light: 6, dark: 4 },
defaultRadius: 'md',
autoContrast: true,
cursorType: 'pointer',
colors: {
brand: virtualColor({
name: 'brand',
light: 'brandLight',
dark: 'brandDark',
}),
brandLight: brandLightColors,
brandDark: brandDarkColors,
},
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
headings: {
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
fontWeight: '700',
},
});

View File

@@ -0,0 +1,5 @@
export { mantineHtmlProps } from '@mantine/core';
export { MantineProvider } from './providers/mantine-provider';
export { MantineColorSchemeScript } from './ui/mantine-color-scheme-script';
export { MantineColorSchemeSwitch } from './ui/mantine-color-scheme-switch';

View File

@@ -0,0 +1,39 @@
'use client';
import {
MantineProvider as BaseMantineProvider,
localStorageColorSchemeManager,
} from '@mantine/core';
import {
MANTINE_COLOR_SCHEME_STORAGE_KEY,
MANTINE_DEFAULT_COLOR_SCHEME,
} from '../config/color-scheme.config';
import { mantineCssVariablesResolver } from '../config/mantine-css-variables-resolver.config';
import { mantineTheme } from '../config/mantine-theme.config';
import type { MantineProviderProps } from '../types/mantine-provider.type';
const colorSchemeManager = localStorageColorSchemeManager({
key: MANTINE_COLOR_SCHEME_STORAGE_KEY,
});
/**
* Провайдер темы и цветовой схемы Mantine.
*
* Используется для:
* - подключения глобальной темы Mantine
* - синхронизации цветовой схемы приложения
*/
export const MantineProvider = (props: MantineProviderProps) => {
const { children } = props;
return (
<BaseMantineProvider
colorSchemeManager={colorSchemeManager}
cssVariablesResolver={mantineCssVariablesResolver}
defaultColorScheme={MANTINE_DEFAULT_COLOR_SCHEME}
theme={mantineTheme}
>
{children}
</BaseMantineProvider>
);
};

View File

@@ -0,0 +1,8 @@
import type { ReactNode } from 'react';
/**
* Пропсы провайдера Mantine UI.
*/
export type MantineProviderProps = {
children: ReactNode;
};

View File

@@ -0,0 +1,22 @@
import { ColorSchemeScript } from '@mantine/core';
import type { ReactNode } from 'react';
import {
MANTINE_COLOR_SCHEME_STORAGE_KEY,
MANTINE_DEFAULT_COLOR_SCHEME,
} from '../config/color-scheme.config';
/**
* Скрипт инициализации цветовой схемы Mantine до гидрации.
*
* Используется для:
* - применения сохраненной цветовой схемы до загрузки React
* - предотвращения мигания темы при первом рендере
*/
export const MantineColorSchemeScript = (): ReactNode => {
return (
<ColorSchemeScript
defaultColorScheme={MANTINE_DEFAULT_COLOR_SCHEME}
localStorageKey={MANTINE_COLOR_SCHEME_STORAGE_KEY}
/>
);
};

View File

@@ -0,0 +1,127 @@
'use client';
import type { MantineColorScheme } from '@mantine/core';
import {
Center,
SegmentedControl,
useMantineColorScheme,
VisuallyHidden,
} from '@mantine/core';
import { useMounted } from '@mantine/hooks';
import type { ReactNode } from 'react';
const colorSchemeValues = [
'auto',
'light',
'dark',
] as const satisfies readonly MantineColorScheme[];
type ColorSchemeValue = (typeof colorSchemeValues)[number];
const systemThemeLabel = (
<Center component="span" h={24} w={28}>
<svg
aria-hidden="true"
fill="none"
height="18"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
viewBox="0 0 24 24"
width="18"
>
<path d="M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v8A2.5 2.5 0 0 1 17.5 16h-11A2.5 2.5 0 0 1 4 13.5z" />
<path d="M9 21h6" />
<path d="M12 16v5" />
</svg>
<VisuallyHidden>Системная тема</VisuallyHidden>
</Center>
);
const lightThemeLabel = (
<Center component="span" h={24} w={28}>
<svg
aria-hidden="true"
fill="none"
height="18"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
viewBox="0 0 24 24"
width="18"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" />
<path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" />
<path d="m19.07 4.93-1.41 1.41" />
</svg>
<VisuallyHidden>Светлая тема</VisuallyHidden>
</Center>
);
const darkThemeLabel = (
<Center component="span" h={24} w={28}>
<svg
aria-hidden="true"
fill="currentColor"
height="18"
viewBox="0 0 24 24"
width="18"
>
<path d="M21 14.31A8.5 8.5 0 0 1 9.69 3a7 7 0 1 0 11.3 11.3Z" />
</svg>
<VisuallyHidden>Темная тема</VisuallyHidden>
</Center>
);
const colorSchemeItems: { label: ReactNode; value: ColorSchemeValue }[] = [
{ label: systemThemeLabel, value: 'auto' },
{ label: lightThemeLabel, value: 'light' },
{ label: darkThemeLabel, value: 'dark' },
];
const isColorSchemeValue = (value: string): value is ColorSchemeValue => {
return colorSchemeValues.some((item) => item === value);
};
/**
* Переключатель цветовой схемы Mantine.
*
* Используется для:
* - выбора системной, светлой или темной темы
* - управления пользовательской цветовой схемой приложения
*/
export const MantineColorSchemeSwitch = (): ReactNode => {
const mounted = useMounted();
const { colorScheme, setColorScheme } = useMantineColorScheme();
const value =
mounted && isColorSchemeValue(colorScheme) ? colorScheme : 'auto';
const handleChange = (nextValue: string): void => {
if (!isColorSchemeValue(nextValue)) {
return;
}
setColorScheme(nextValue);
};
return (
<SegmentedControl
aria-label="Цветовая схема"
color="brand"
data={colorSchemeItems}
onChange={handleChange}
radius="xl"
size="sm"
value={value}
withItemsBorders={false}
/>
);
};

View File

@@ -1 +0,0 @@
export { MantineProvider } from './mantine-provider';

View File

@@ -1,11 +0,0 @@
'use client';
import { MantineProvider as BaseMantineProvider } from '@mantine/core';
import type { PropsWithChildren } from 'react';
/**
* Провайдер Mantine UI.
*/
export const MantineProvider = ({ children }: PropsWithChildren) => {
return <BaseMantineProvider>{children}</BaseMantineProvider>;
};

View File

@@ -1,8 +1,13 @@
import { Container, Image, Stack, Text, Title } from '@mantine/core'; import { Container, Image, Stack, Text, Title } from '@mantine/core';
import { MantineColorSchemeSwitch } from 'infra/mantine';
import styles from './styles/home.module.css'; import styles from './styles/home.module.css';
/** /**
* Главный экран приложения. * Главный экран стартовой страницы приложения.
*
* Используется для:
* - отображения приветственного содержимого шаблона
* - предоставления быстрого переключения цветовой схемы
*/ */
export const HomeScreen = () => { export const HomeScreen = () => {
return ( return (
@@ -18,6 +23,7 @@ export const HomeScreen = () => {
/> />
<Title order={1}>Добро пожаловать</Title> <Title order={1}>Добро пожаловать</Title>
<Text c="dimmed">Шаблон приложения на Next.js и TypeScript.</Text> <Text c="dimmed">Шаблон приложения на Next.js и TypeScript.</Text>
<MantineColorSchemeSwitch />
</Stack> </Stack>
</Container> </Container>
</div> </div>

View File

@@ -21,7 +21,7 @@
"paths": { "paths": {
"app/*": ["./src/app/*"], "app/*": ["./src/app/*"],
"business/*": ["./src/business/*"], "business/*": ["./src/business/*"],
"infrastructure/*": ["./src/infrastructure/*"], "infra/*": ["./src/infra/*"],
"layouts/*": ["./src/layouts/*"], "layouts/*": ["./src/layouts/*"],
"screens/*": ["./src/screens/*"], "screens/*": ["./src/screens/*"],
"ui/*": ["./src/ui/*"], "ui/*": ["./src/ui/*"],