Files

1331 lines
72 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: style-guide
description: "Используй при создании, изменении, форматировании или ревью frontend-кода и физическом оформлении уже спроектированных SLM-модулей по code style и style guide. Триггеры: JS, TS, JSX, TSX, HTML, CSS, .js, .ts, .jsx, .tsx, .module.css, React-компонент, Provider, Guard, Error Boundary, props, components/, providers/, modules/, ErrorCode, код ошибки, SCREAMING_SNAKE_CASE, стили, CSS Modules, JSDoc, документация, именование, импорты/экспорты, типизация, отформатировать по гайду, поправить кодстайл. НЕ используй для выбора владельца, слоя, модульной границы или public API по SLM, генерации .templates, Next.js routing/data fetching/rendering, настройки PostCSS/tooling, backend-кода и вопросов без правки frontend-кода; в смешанной задаче сначала примени slm-design."
metadata:
internal: true
---
<!-- Generated from src/SKILL.md. Do not edit manually. -->
# Style guide
## База style guide
Применяй этот канон ко всем файлам перед языковыми правилами. Если правило языка уточняет базовое правило, используй языковое правило.
### Порядок применения
- Сначала проверь formatter, linter, `editorconfig`, `tsconfig` и локальный стиль затронутых файлов.
- Если formatter или linter задаёт формат, следуй ему даже при отличии от этого канона.
- Не меняй formatter, linter или `tsconfig` в задаче на обычную правку кода.
- Меняй стиль только в затронутом коде. Не форматируй весь проект без отдельной задачи.
- Сохраняй локальную консистентность, если она не конфликтует с автоматическими проверками.
- Не переименовывай публичные сущности без отдельной задачи на переименование.
### Базовый формат
- Используй 2 пробела. Не используй табы.
- Ориентируйся на 120 символов в строке, если проект не задаёт другой `lineWidth`.
- Не переноси читаемую строку механически только ради лимита.
- Переноси выражение на новые строки, когда строка становится плохо читаемой.
- Не переноси строку внутри строкового литерала без необходимости.
### Именование
Следуй этим форматам, если проект не задаёт другой локальный стандарт.
| Сущность | Формат |
| --- | --- |
| Папки и файлы | `kebab-case` |
| Переменные и функции | `camelCase` |
| Классы, типы, React-компоненты | `PascalCase` |
| Константы верхнего уровня | `SCREAMING_SNAKE_CASE` |
| Собственные коды ошибок | `SCREAMING_SNAKE_CASE` |
| Хуки | `useSomething` |
| CSS-классы в CSS Modules | `camelCase` |
| Enum-ключи | `SCREAMING_SNAKE_CASE` |
### Файлы
- Суффикс нужен, чтобы быстро определить роль или тип открытого файла без контекста дерева папок.
- Суффикс пишется в единственном числе.
- Формат ролевого файла: `name.<suffix>.ts` или `name.<suffix>.tsx`.
- Список ниже - примеры типовых суффиксов, а не закрытый перечень.
- Если роли файла нет в списке, добавь понятный суффикс по тому же принципу и сохраняй локальную консистентность проекта.
- Не переименовывай публичные файлы без отдельной задачи на переименование.
Типовые суффиксы:
- `use-name.hook.ts` - файл хука, функция именуется `useName`.
- `.store.ts` - store.
- `.service.ts` - сервис.
- `.type.ts` - типы и интерфейсы.
- `.interface.ts` - интерфейсы, если проект отделяет их от типов.
- `.enum.ts` - enum.
- `.dto.ts` - внешние DTO.
- `.schema.ts` - схемы валидации.
- `.constant.ts` - константы.
- `.config.ts` - конфигурация.
- `.util.ts` - утилиты.
- `.helper.ts` - вспомогательные функции.
- `.lib.ts` - библиотечный код.
- `.test.ts` или `.test.tsx` - тесты.
- `.mock.ts` - моки.
### Общие правила комментариев
- Комментарий объясняет назначение, сценарий применения, ограничение или неочевидную причину.
- Не добавляй комментарий, который дословно пересказывает код.
- Однострочный комментарий отделяй пробелом после маркера.
- Завершай комментарий точкой, если это фраза или предложение.
- Заголовок комментария пиши по правилам русского языка: с заглавной только первая буква, кроме имён собственных и аббревиатур.
- Если комментарий состоит из заголовка и тела, отделяй заголовок от тела одной пустой строкой.
- Логические части внутри многострочного комментария отделяй одной пустой строкой.
- Удаляй устаревший комментарий при изменении поведения кода.
### Чеклист базы
- Formatter, linter, `editorconfig`, `tsconfig` и локальный стиль файла соблюдены.
- Diff не содержит форматирования незатронутого кода.
- Отступы, длина строк и переносы не создают альтернативный стиль.
- Имена соответствуют типу сущности и локальному стандарту проекта.
- Комментарии объясняют причину или назначение, а не пересказывают код.
## Физическая структура SLM
Сначала определи архитектурную роль единицы по правилам `slm-design`. Этот раздел не выбирает владельца и не решает, нужен компонент или вложенный модуль: он задаёт обязательный путь после принятого архитектурного решения.
### Сопоставление единиц с каталогами
- Вложенные framework-компоненты, кроме `Provider`, размещай в `components/`.
- `Guard` и `Error Boundary` считай framework-компонентами и размещай в `components/`.
- Вложенные `Provider` размещай в `providers/`, а не в `components/`.
- Вложенные SLM-модули размещай в `modules/`.
- Внутри модуля не используй `ui/`, `parts/` или другой каталог как альтернативное имя для `components/`, `providers/` или `modules/`.
- Не создавай эти каталоги заранее: добавляй каталог, когда в модуле появляется соответствующая единица.
```text
checkout/
├── index.ts
├── checkout.tsx
├── styles/
├── types/
├── components/
│ ├── order-summary/
│ ├── checkout-guard/
│ └── checkout-error-boundary/
├── providers/
│ └── checkout-events-provider/
└── modules/
└── form-session/
```
### Корневой framework-файл
Один главный framework-файл модуля может находиться непосредственно в корне, если он реализует или собирает ответственность модуля. Исключение распространяется и на главный `Provider`.
Корневой файл не дублируй в `components/` или `providers/`. Остальные компоненты и providers этого модуля размещай в соответствующих каталогах.
### Ограничение вложенности
- Единицы внутри `components/` и `providers/` являются соседями относительно модуля.
- Каталог компонента или Provider не содержит собственные `components/`, `providers/` или `modules/`.
- Следующий структурный уровень создавай только через вложенный модуль в `modules/`.
- Runtime-вложенность JSX не повторяй файловой вложенностью: компонент может рендерить другой компонент, оставаясь его файловым соседом.
### Чеклист структуры SLM
- Архитектурная роль определена до выбора каталога.
- Компоненты, Guards и Error Boundaries находятся в `components/`.
- Вложенные Providers находятся в `providers/`.
- Вложенные модули находятся в `modules/`.
- Главный framework-файл является единственной корневой implementation-единицей модуля.
- Компонентные каталоги не создают рекурсивную файловую иерархию.
## JS/TS
Применяй этот раздел к `.js`, `.ts` и JS/TS-частям `.jsx` и `.tsx`. Для React-компонентов дополнительно применяй раздел `React и TSX`, для разметки - раздел `HTML, JSX и TSX`.
### Строки
- Для обычных строк используй одинарные кавычки.
- Строки в обратных кавычках используй только для интерполяции `${...}` или многострочного текста.
```ts
const label = 'Сохранить';
const title = `Привет, ${name}`;
```
### Импорты и экспорты
- В именованных импортах ставь пробелы внутри фигурных скобок: `import { User } from './user'`.
- Используй `import type`, если импорт нужен только на уровне типов.
- Для собственного кода предпочитай именованные экспорты.
- `default export` используй только когда это требует framework, tooling или устоявшийся локальный стандарт.
- `default import` допустим для сторонних библиотек, CSS Modules и framework API.
- Избегай namespace imports вида `import * as api`, если библиотека или локальный стандарт этого не требует.
- Не импортируй глубже публичного API модуля, если проектная архитектура это запрещает.
- Не меняй направление импортов ради удобства. Для архитектурных ограничений используй `slm-design`.
- Не меняй порядок, путь или тип импортов только ради форматирования, кроме автоформатирования существующим formatter/linter.
- Не создавай новый barrel или public API без понимания архитектурной границы.
- Экспорт типов оформляй через `export type`, если экспортируется только тип.
```ts
import { createUser } from './create-user';
import type { User } from './user.type';
export { UserCard } from './user-card';
export type { UserCardProps } from './types/user-card-props.type';
```
### Объекты, массивы, коллекции и вызовы
- Короткие объекты, массивы и вызовы оставляй в одну строку, если они читаются.
- В многострочном объекте размещай каждое свойство на новой строке.
- В многострочном массиве размещай каждый элемент на новой строке.
- В длинном вызове функции размещай каждый аргумент или смысловую группу на новой строке.
- В однострочных объектах и массивах ставь пробелы после запятых.
- Массивы называй во множественном числе.
- Списки идентификаторов называй с суффиксом `Ids`.
- Словари и мапы называй с суффиксом `ById`, `Map` или `Dict`.
```ts
const roles = ['admin', 'editor', 'viewer']
const options = { id: 1, name: 'User' }
const userIds = ['u1', 'u2']
const usersById = {} as Record<string, User>
const config = createRequestConfig(
endpoint,
{
headers: {
'X-Request-Id': requestId,
'X-User-Id': userId
},
params: {
page,
pageSize,
sort: 'createdAt'
}
},
timeoutMs
)
```
### Точки с запятой и trailing comma
- По умолчанию не ставь точку с запятой в конце инструкций.
- По умолчанию не ставь trailing comma после последнего свойства объекта, элемента массива, аргумента или параметра.
- Если formatter, linter или локальный стиль файла задаёт другое поведение, следуй ему.
- Не делай отдельную правку только ради массового добавления или удаления точек с запятой и trailing comma вне затронутого кода.
### Early return
- Используй ранние возвраты для упрощения чтения.
- Избегай `else` после `return`.
```ts
const getName = (user?: { name: string }) => {
if (!user) {
return 'Гость';
}
return user.name;
};
```
### Булевые значения
- Булевые значения начинай с `is`, `has`, `can` или `should`.
- Не используй нейтральные имена вроде `ready`, `access`, `submit`, если по имени неясно, что это boolean.
```ts
const isReady = true;
const hasAccess = false;
const canSubmit = true;
const shouldRedirect = false;
```
### События и callback
- Обработчики внутри кода называй через `handle*`.
- Callback-параметры и callback-свойства называй через `on*`.
```ts
const handleSubmit = () => {
// ...
};
type FormOptions = {
onSubmit: () => void;
};
```
### TypeScript
- Указывай типы для параметров функций и компонентов.
- Для публичных функций указывай возвращаемый тип.
- Не полагайся на неявный вывод типов для публичных API и важных контрактов.
- Предпочитай `type` для описания сущностей и `interface` для расширяемых контрактов.
- Избегай `any` и `unknown` без необходимости.
- Не используй `// @ts-ignore`, кроме крайних случаев с явным комментарием причины.
- Если нужно временно подавить ошибку TypeScript, предпочитай `// @ts-expect-error` с объяснением причины.
```ts
/**
* Форматирует цену с символом валюты.
*/
export const formatPrice = (value: number): string => {
return `${value} ₽`;
};
```
### Type vs interface
- Используй `type` для props, DTO, view-model, unions, mapped types и композиции типов.
- Используй `interface`, когда контракт должен расширяться через `extends` или declaration merging.
- Не смешивай `type` и `interface` в одной области без причины.
```ts
export type UserCardProps = {
user: User;
onSelect: (userId: string) => void;
};
export interface StorageAdapter {
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<void>;
}
```
### Any и unknown
- `any` используй только как временную заглушку или при работе с внешним API, который невозможно типизировать сразу.
- Каждый `any` должен иметь понятную причину и план замены, если это не неизбежная граница интеграции.
- `unknown` используй на границах с внешними данными и обязательно сужай перед использованием.
```ts
const parseName = (value: unknown): string => {
if (typeof value === 'string') {
return value;
}
return '';
};
```
Плохо:
```ts
const parseName = (value: any) => value.trim();
```
### Predicates и narrowing
- Для runtime-проверок базовых значений используй `shared/lib/value-predicates`.
- Если в проекте нет `shared/lib/value-predicates`, создай библиотеку перед использованием predicates. Состав утилит, сигнатуры и поведение бери из `reference/value-predicates` внутри skill.
- Импортируй predicates из публичного API библиотеки, а не из внутренних файлов.
- `shared/lib/value-predicates` содержит только базовые проверки nullish, primitives, strings, arrays, objects и literal unions.
- Доменные guards вроде `isOrder`, `isCity`, `isUserError` размещай рядом с владельцем данных: business-модулем, mapper/source или infra-адаптером.
- Не размещай доменные guards в `shared/lib/value-predicates`.
Минимальная структура библиотеки:
```text
shared/lib/value-predicates/
├── index.ts
└── value-predicates.ts
```
```ts
import { hasOwn, isArrayOf, isNonEmptyArray, isRecord, isString } from 'shared/lib/value-predicates'
```
Predicates применяй, когда нужно проверить:
- значение определено или отсутствует;
- значение является `string`, `number` или `boolean`;
- строка является непустой;
- массив пустой или непустой;
- `unknown`-значение является массивом элементов нужной формы;
- `unknown`-значение является объектом-записью;
- значение входит в список допустимых литералов.
Для определения пустого или непустого списка не используй прямые проверки длины массива:
```ts
items?.length
items.length > 0
items.length !== 0
items.length === 0
!items.length
```
Используй predicates:
```ts
isEmptyArray(items)
isNonEmptyArray(items)
```
Для `unknown`-данных и внешних API-ответов не используй `Array.isArray(value)` как проверку формы элементов. Используй `isArrayOf` с item guard:
```ts
if (isArrayOf(value, isOrder)) {
// value: Order[]
}
```
Для object-like `unknown` используй `isRecord` и `hasOwn` перед чтением полей:
```ts
const isOrder = (value: unknown): value is Order => {
return isRecord(value) && hasOwn(value, 'id') && isString(value.id)
}
```
Для literal union используй `isOneOf`:
```ts
const statuses = ['draft', 'published'] as const
if (isOneOf(value, statuses)) {
// value: 'draft' | 'published'
}
```
Прямое обращение к `.length` допустимо, когда нужен именно числовой размер:
```ts
const count = items.length
if (password.length < 8) {
return 'Минимум 8 символов'
}
for (let index = 0; index < items.length; index += 1) {
// ...
}
```
Прямой `map` допустим, если массив заранее нормализован:
```ts
const items = data ?? []
return items.map((item) => mapItem(item))
```
### Type assertions
- Не используй `as` для обхода TypeScript без проверки данных.
- Допускай `as const` для литеральных конфигураций и enum-like объектов.
- Type assertion на границе внешних данных должен сопровождаться проверкой, схемой валидации или явной причиной.
```ts
const sortDirections = ['asc', 'desc'] as const;
export type SortDirection = (typeof sortDirections)[number];
```
### Публичный API
- Если файл является public API, экспортируй только то, что разрешено наружу.
- Не экспортируй внутренние helpers, временные типы и implementation details.
- Если задача требует изменить public API слоя или модуля, сначала применяй `slm-design`.
### Документирование JS/TS
#### Базовые правила
- Документируй точку объявления именованной JS/TS-сущности.
- JSDoc ставь непосредственно перед объявлением функции, класса, типа, интерфейса, enum или документируемой константы.
- Если функция объявлена через `const name = () => {}`, комментарий ставь перед `const`.
- Документируй назначение сущности, а не синтаксис её объявления.
- Объясняй, зачем нужна сущность, какой сценарий она закрывает или какое ограничение важно знать.
- Не документируй использование сущности, вызовы функций и строки внутри тела кода.
- Не дублируй TypeScript-сигнатуру.
- Не описывай параметры, возвращаемые значения и props через `@param`, `@returns` или `@type`, если они уже видны из типов.
- Если назначение очевидно из имени и типа, всё равно добавь короткий JSDoc без пересказа сигнатуры.
- React-компоненты документируй по правилам раздела `React и TSX`.
#### Базовый шаблон многострочного комментария
- Для документирования JS/TS-сущностей используй многострочный JSDoc-комментарий.
```ts
/**
* <Что делает сущность в 1 строке>.
*
* <Опционально: описание сложной механики или важных нюансов>.
*/
```
#### Функции
- Каждая именованная функция должна иметь многострочный JSDoc-комментарий.
- Под правило попадают `function name()`, `const name = () => {}`, `const name = function () {}` и именованные обработчики вроде `const handleSubmit = () => {}`.
- Комментарий функции описывает действие, результат, побочный эффект или важное ограничение.
```ts
/**
* Рекурсивно собирает дерево категорий из плоского списка.
*
* Группирует элементы по parentId, начиная с корневых категорий.
* Категории без родителя попадают в корень дерева.
*/
export const buildCategoryTree = (categories: Category[]): CategoryTree[] => {
// ...
}
```
#### Классы
- Каждый класс должен иметь многострочный JSDoc-комментарий.
- Комментарий класса описывает роль класса и границу ответственности.
- Не перечисляй методы класса, если их назначение понятно из public API.
```ts
/**
* Клиент для работы с заказами через REST API.
*
* Инкапсулирует маршруты, сериализацию параметров и обработку ответа.
*/
export class OrdersClient {
// ...
}
```
#### Типы и интерфейсы
- Каждый `type` и `interface` должен иметь многострочный JSDoc-комментарий.
- Комментарий типа или интерфейса описывает контракт: модель, DTO, props, config, состояние или внешний формат.
- Каждое поле `type` или `interface` должно иметь JSDoc-комментарий `/** ... */` непосредственно над полем.
- Комментарий поля описывает смысл значения, доменное ограничение, формат или сценарий использования.
- Не объединяй одним комментарием несколько полей.
- Не отделяй поля пустой строкой только из-за JSDoc-комментария.
- Пустую строку между полями добавляй только для смыслового разделения групп полей.
- Если поле кажется очевидным, всё равно добавь короткое описание без пересказа имени.
```ts
/**
* Фильтры списка заказов.
*/
export type OrderFilters = {
/** Идентификатор пользователя-владельца заказов. */
userId?: string
/** Статус заказа для фильтрации выдачи. */
status?: OrderStatus
}
```
#### Коды ошибок
- Собственные коды ошибок проекта записывай в `SCREAMING_SNAKE_CASE` независимо от способа их представления.
- Не переименовывай непрозрачный код внешнего источника: сначала интерпретируй его на границе владельца и сопоставь с собственным кодом проекта.
- Каждый type, объявляющий набор кодов ошибок, документируй многострочным JSDoc.
- Каждый литерал в union кодов ошибок снабжай отдельным JSDoc непосредственно перед литералом.
- Комментарий литерала объясняет ожидаемый исход, условие его возникновения или решение, которое по нему принимает потребитель.
- Не пересказывай в комментарии имя кода и не объединяй несколько кодов одним комментарием.
```ts
/**
* Ожидаемые неуспешные исходы сценариев заказа.
*/
export type OrderErrorCode =
/** Товар изменился после добавления в черновик заказа. */
| 'PRODUCT_CHANGED'
/** Запрошенное количество товара отсутствует на складе. */
| 'INSUFFICIENT_STOCK'
/** Заказ нельзя отменить в его текущем состоянии. */
| 'CANNOT_CANCEL'
```
#### Константы
- Документируй константу только если она является публичным контрактом, доменным ограничением, magic value или переиспользуемой конфигурацией.
- Обычные локальные константы не документируй.
```ts
/**
* Максимальное количество заказов на одной странице выдачи.
*
* Значение синхронизировано с ограничением backend API.
*/
export const MAX_ORDERS_PAGE_SIZE = 100
```
#### Enum
- Каждый `enum` должен иметь многострочный JSDoc-комментарий.
- Комментарий enum описывает назначение набора значений.
- Каждое значение enum должно иметь JSDoc-комментарий `/** ... */` непосредственно над значением.
- Комментарий значения enum описывает смысл значения или состояние, которое оно обозначает.
- Не отделяй значения enum пустой строкой только из-за JSDoc-комментария.
- Не оставляй значение enum без комментария, даже если оно кажется очевидным.
```ts
/**
* Состояние оплаты заказа.
*/
export enum PaymentStatus {
/** Оплата создана, но ещё не подтверждена провайдером. */
PENDING = 'pending',
/** Оплата успешно подтверждена провайдером. */
PAID = 'paid',
/** Оплата отклонена или завершилась ошибкой. */
FAILED = 'failed'
}
```
#### Что не документировать
- Не документируй обычные переменные и inline callback внутри вызовов вроде `useEffect`, `map`, `filter`, `reduce`, `forEach`, `then` или обработчиков API.
- Если inline callback требует пояснения, вынеси его в именованную функцию и задокументируй объявление.
#### Плохо
```ts
/**
* @param value - число.
* @returns строка с ценой.
*/
export const formatPrice = (value: number): string => {
return `${value} ₽`
}
```
### Чеклист JS/TS
- Строки, объекты, массивы, коллекции, вызовы, semicolon и trailing comma соответствуют дефолту или formatter/linter.
- Early return упрощает чтение и не меняет поведение.
- Булевые значения и callback названы по JS/TS-соглашениям.
- Параметры функций типизированы; публичные функции имеют возвращаемый тип.
- `any`, `unknown`, `as` и suppress-комментарии имеют явную причину.
- Если `shared/lib/value-predicates` отсутствует, библиотека создана по `reference/value-predicates` перед использованием predicates.
- Проверки пустых и непустых массивов используют `shared/lib/value-predicates`, а не прямые `.length`-условия.
- `unknown` массивы проверяются через `isArrayOf(value, itemGuard)`, если важен тип элементов.
- Object-like `unknown` проверяется через `isRecord` и `hasOwn` перед чтением полей.
- Доменные guards размещены рядом с владельцем данных, а не в `shared/lib/value-predicates`.
- Импорты типов оформлены через `import type`, а экспорты типов через `export type`.
- Public API не раскрывает implementation details.
- Собственные error codes записаны в `SCREAMING_SNAKE_CASE`, а каждый код имеет отдельный содержательный JSDoc.
- JS/TS-функции, классы, типы, интерфейсы, поля типов, enum, значения enum и константы-контракты документированы по правилам своего подраздела.
## React и TSX
Этот раздел описывает прикладную форму React framework-компонента после того, как архитектура уже определила его владельца и место. Правила распространяются на визуальные компоненты, Providers, Guards и Error Boundaries. Архитектурные границы компонентов, модулей, сегментов и public API определяет `slm-design`. Форму JSX-разметки дополнительно проверяй по разделу `HTML, JSX и TSX`.
### Обязательный обвес framework-компонента
- Используй эту форму для каждого React framework-компонента, включая Provider, Guard и Error Boundary.
- Каждый вложенный framework-компонент размещай в отдельной папке согласно разделу `Физическая структура SLM`.
- Держи `.tsx`, props-типы, CSS Module и локальный `index.ts` в отдельных файлах.
- Файлы структуры обязательны даже для невизуального компонента без собственного DOM: `.tsx`, `types/`, `styles/`, `index.ts`.
- Не объявляй props-типы внутри `.tsx`.
- Не размещай CSS рядом с TSX-кодом: стили живут в `styles/{name}.module.css`.
- Для компонента с собственным DOM корневой CSS-класс называется `.root`.
- Для невизуального компонента создай пустой CSS Module, но не импортируй его до появления стилизуемого DOM.
- Не добавляй Provider, Guard или другой невизуальный компонент DOM-wrapper только ради использования CSS Module.
- Компонент обязан иметь JSDoc-комментарий по React-шаблону.
- Всё, что не является React-компонентом, документируй по правилам `Документирование JS/TS`.
### Структура файлов
- Вложенный framework-компонент живёт в собственной папке.
- Имя файла props-типа строится как `{name}-props.type.ts`.
- CSS Module строится как `{name}.module.css`.
- Локальный `index.ts` экспортирует компонент и props-тип для использования внутри модуля.
- Единственное исключение из собственной папки — главный framework-файл модуля, включая главный Provider.
- Для главного framework-файла папкой компонента является корень модуля: `styles/` и `types/` создаются непосредственно в нём.
- Корневой `index.ts` остаётся публичным фасетом модуля и экспортирует компонент только через фасет, разрешённый архитектурой и средой выполнения.
```text
user-status/
├── styles/
│ └── user-status.module.css
├── types/
│ └── user-status-props.type.ts
├── user-status.tsx
└── index.ts
```
Корневой framework-файл использует тот же обвес без дополнительной папки:
```text
module/
├── index.ts
├── <root-framework-file>.tsx
├── styles/
│ └── {name}.module.css
└── types/
└── {name}-props.type.ts
```
### Типизация props
- Props-типы выноси в `types/{name}-props.type.ts`.
- Собственные параметры компонента называй `{Name}Params`.
- Атрибуты собственного корневого HTML-элемента называй `RootAttrs`.
- Итоговый тип props называй `{Name}Props`.
- Для компонента с собственным DOM типизируй `RootAttrs` через `ComponentPropsWithoutRef<'tag'>`.
- Для Provider или другого компонента без собственного DOM не создавай фиктивный `RootAttrs`.
- Исключай из `RootAttrs` атрибуты, которыми компонент управляет сам: например `children`, если компонент рендерит собственный контент.
- Если собственный параметр конфликтует с HTML-атрибутом, исключай этот атрибут через `Omit`.
- Документируй все объявленные `Params`, `RootAttrs`, `Props` и каждое поле props по правилам `JS/TS`.
```ts
import type { ComponentPropsWithoutRef } from 'react'
/**
* Собственные параметры UserStatus.
*/
export type UserStatusParams = {
/** Текст статуса пользователя. */
label: string
/** Доступен ли пользователь сейчас. */
isOnline: boolean
}
/**
* Атрибуты корневого span без children.
*/
type RootAttrs = Omit<ComponentPropsWithoutRef<'span'>, 'children'>
/**
* Props UserStatus.
*/
export type UserStatusProps = RootAttrs & UserStatusParams
```
### Реализация TSX
- В `.tsx` держи сам компонент и импорт props-типа.
- CSS Module и функцию склейки классов импортируй только для компонента с собственным стилизуемым DOM.
- Функцию склейки классов импортируй как `cl`: `import cl from 'clsx'`.
- Функциональный компонент объявляй через `const` и именованный экспорт, если framework или локальный стандарт не требует другой формы.
- Нативный React Error Boundary разрешено объявлять классом, потому что React не предоставляет функциональный lifecycle для перехвата ошибок дочернего дерева.
- Не используй `React.FC` по умолчанию.
- Параметр функционального компонента называй `props` и типизируй через `{Name}Props`.
- Возвращаемый тип компонента не указывай: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата.
- Деструктурируй props функционального компонента внутри тела, а не в сигнатуре.
- Из props выделяй `className` и `...rootAttrs`, если компонент имеет собственный корневой DOM-элемент и принимает его HTML-атрибуты.
- Прокидывай `rootAttrs` на собственный корневой DOM-элемент.
- Собственный корневой DOM-элемент обязан получить `styles.root` первым CSS-классом.
- Внешний `className` добавляй последним аргументом в `cl(...)`.
- Не добавляй невизуальному компоненту DOM-элемент, `className` или `rootAttrs`, если они не нужны его контракту.
- Не создавай вложенный компонент внутри render без причины.
- Не дублируй условия в JSX, если их можно выразить через заранее подготовленную переменную с понятным именем.
- Для сложной разметки сначала упрощай данные и условия, потом JSX.
```tsx
import cl from 'clsx'
import type { UserStatusProps } from './types/user-status-props.type'
import styles from './styles/user-status.module.css'
/**
* Статус пользователя в карточке профиля.
*
* Используется для:
* - отображения текущей доступности пользователя
* - визуального выделения онлайн- и офлайн-состояний
*/
export const UserStatus = (props: UserStatusProps) => {
const { label, isOnline, className, ...rootAttrs } = props
return (
<span
{...rootAttrs}
className={cl(styles.root, isOnline && styles.online, className)}
>
{label}
</span>
)
}
```
### Props и events
- Callback props называй через `on*`: `onSubmit`, `onChange`, `onClose`.
- Внутренние обработчики называй через `handle*`: `handleSubmit`, `handleChange`, `handleClose`.
- Не прокидывай событие DOM наружу, если внешний код должен знать только бизнес-смысл действия.
- Boolean props называй через `is*`, `has*`, `can*` или `should*`.
- Named handlers документируй как функции по правилам `JS/TS`.
- Inline callback в JSX props не документируй.
### Hooks
- Соблюдай Rules of Hooks: вызывай hooks только на верхнем уровне компонента или другого hook.
- Не используй `useEffect` для вычисления данных, которые можно получить из props/state во время render.
- Выноси повторяемую stateful-логику в custom hook только когда она реально переиспользуется или упрощает компонент.
- Custom hook называй через `use*`.
- Вызовы `useEffect`, `useMemo`, `useCallback` и других hooks не документируй.
- Inline callback внутри hooks не документируй.
- Если логика hook требует комментария, вынеси её в именованную функцию или custom hook и документируй объявление по `JS/TS`.
- Custom hook документируй как именованную функцию по `JS/TS`.
### Стили
- CSS Module всегда создавай в `styles/{name}.module.css`.
- Если компонент имеет собственный DOM, корневой CSS-класс называй `.root`.
- `.root` описывает реальный корневой DOM-элемент компонента.
- Дополнительные классы описывай рядом с `.root` и подключай через `styles.*`.
- Не подменяй `.root` внешним `className`: внешний класс только дополняет `styles.root`.
- CSS Module невизуального компонента оставляй пустым и не импортируй, пока у компонента не появится собственный стилизуемый DOM.
- Не создавай DOM-wrapper только для применения `.root`.
```css
.root {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--color-text-muted);
}
```
### Локальный экспорт
- В локальном `index.ts` экспортируй компонент и props-тип.
- Компонент экспортируй обычным named export.
- Props экспортируй через `export type`.
- Не экспортируй `Params` и `RootAttrs` наружу без необходимости.
- Для корневого framework-файла используй публичный фасет модуля и не обходи его требования к среде выполнения.
```ts
export { UserStatus } from './user-status'
export type { UserStatusProps } from './types/user-status-props.type'
```
### Документирование компонентов
- Каждый React framework-компонент, включая Provider, Guard и Error Boundary, должен иметь многострочный JSDoc-комментарий.
- Комментарий ставь непосредственно перед объявлением компонента.
- Компонент документируй по React-шаблону, а не по шаблону функции из `JS/TS`.
- Нативный Error Boundary, объявленный классом, также документируй по React-шаблону как компонент.
- Компонент описывает назначение и сценарии применения.
- Комментарий должен помогать понять, когда использовать компонент, без чтения реализации.
- В `Используется для` указывай только реальные сценарии применения.
- Добавляй минимум один сценарий; верхнего ограничения по количеству сценариев нет.
- Не добавляй сценарии ради количества.
- Не описывай реализацию вида `рендерит div с className`, если это видно из кода.
- Всё, что не является React-компонентом, документируй по правилам `Документирование JS/TS`.
- Props-типы, типы, enum, классы, функции, константы-контракты и named handlers подчиняются правилам `JS/TS`.
- Вызовы `useEffect`, `useMemo`, `useCallback` и других hooks не документируй.
- Inline callback внутри hooks, `map`, `filter`, event props и других вызовов не документируй.
- Если inline callback требует пояснения, вынеси его в именованную функцию и задокументируй объявление по `JS/TS`.
Шаблон:
```tsx
/**
* <Назначение компонента в 1 строке>.
*
* Используется для:
* - <сценарий 1>
* - <сценарий 2, если есть>
*/
```
Плохо:
```tsx
/**
* Рендерит span с className и label.
*/
export const UserStatus = (props: UserStatusProps) => {
// ...
}
```
### Чеклист React/TSX
- Каждый framework-компонент имеет `.tsx`, `types/`, `styles/` и локальный `index.ts`; главный framework-файл использует корень и фасет модуля.
- Props вынесены в `types/{name}-props.type.ts`; `RootAttrs` добавлен только компоненту с собственным DOM.
- Props-типы и поля props документированы по правилам `JS/TS`.
- Функциональный компонент объявлен через `const`, принимает `props` и деструктурирует их внутри тела; нативный Error Boundary может быть классом.
- Компонент не использует `React.FC` по умолчанию и не указывает возвращаемый тип без необходимости.
- Собственный корневой DOM-элемент получает `{...rootAttrs}` и `className={cl(styles.root, ..., className)}`.
- CSS Module лежит в `styles/{name}.module.css`; у компонента с DOM корневой класс называется `.root`.
- Невизуальный компонент имеет пустой CSS Module без неиспользуемого импорта и искусственного DOM-wrapper.
- Локальный `index.ts` экспортирует компонент и props-тип; корневой компонент экспортируется через подходящий фасет модуля.
- Каждый framework-компонент имеет комментарий по React-шаблону с назначением и сценариями применения.
- Callback props названы через `on*`, внутренние обработчики - через `handle*`.
- Named handlers и custom hooks документированы по правилам `JS/TS`; вызовы hooks и inline callbacks не документируются.
- Hooks вызваны только на верхнем уровне компонента или custom hook.
## HTML, JSX и TSX
Применяй этот раздел к HTML-разметке и JSX/TSX-деревьям. Для React-компонентов дополнительно применяй раздел `React и TSX`, для JS/TS-выражений внутри JSX/TSX - раздел `JS/TS`.
### Атрибуты и props
- HTML-теги и HTML-атрибуты пиши в нижнем регистре.
- Строковые значения HTML/JSX/TSX-атрибутов пиши в двойных кавычках.
- Динамические значения передавай через `{...}`.
- Статические boolean-атрибуты записывай без `={true}`.
- Динамические boolean-атрибуты записывай через выражение: `disabled={isSaving}`.
- Не добавляй пустые, дублирующиеся или неиспользуемые атрибуты.
- Props компонентов именуй по правилам `React и TSX`.
```tsx
<button
type="button"
disabled={isSaving}
onClick={handleSave}
>
Сохранить
</button>
```
### Формат элемента
- Короткий элемент с небольшим числом атрибутов или props можно оставлять в одну строку.
- Если атрибутов или props много, размещай каждый на отдельной строке.
- Закрывающую скобку многострочного элемента размещай на отдельной строке.
- Если элемент содержит вложенные элементы, размещай открывающий и закрывающий теги на отдельных строках.
- Не смешивай в одном участке разметки разные стили записи props без причины.
```tsx
<input type="text" name="email" />
<UserCard
user={user}
isSelected={isSelected}
onSelect={handleSelect}
/>
```
### Условия и значения в разметке
- Не усложняй JSX/TSX условиями, которые можно подготовить в переменной до `return`.
- Не размещай составные runtime-условия непосредственно в JSX/TSX.
- Не используй тернарные выражения внутри JSX/TSX-разметки.
- Подготовь label, локальные данные, boolean-флаги, списки и ветки отображения до JSX.
- В JSX используй простые predicate gates или заранее подготовленные флаги.
- Если для JSX-условий нужны predicates, но в проекте нет `shared/lib/value-predicates`, сначала создай библиотеку по `reference/value-predicates`.
- Если ветвление большое, вынеси его в переменную, named function или отдельный компонент по правилам `React и TSX`.
- Render-переменные вида `profileContent`, `ordersContent`, `emptyStateContent` используй только когда ветка большая, повторяется или содержит несколько состояний.
- Если условие простое, не создавай render-переменную без необходимости.
- Не дублируй длинные пути к данным внутри JSX: вынеси значение в локальную переменную до `return`.
- Inline callback в JSX props не документируй; если ему нужно пояснение, вынеси в named handler.
```tsx
const submitLabel = isSaving ? 'Сохраняем' : 'Сохранить'
const profileUser = currentUser.error ? null : currentUser.data
const ordersData = orders.data
const shouldShowEmptyState = isEmptyArray(ordersData) && !orders.isLoading
return (
<>
<button type="submit" disabled={isSaving}>
{submitLabel}
</button>
{isDefined(profileUser) && (
<Profile user={profileUser} />
)}
{isNonEmptyArray(ordersData) && ordersData.map((order) => (
<OrderCard key={order.id} order={order} />
))}
{shouldShowEmptyState && (
<EmptyState />
)}
</>
)
```
Плохо:
```tsx
return (
<>
<button type="submit">
{isSaving ? 'Сохраняем' : 'Сохранить'}
</button>
{data && !currentUser.error && (
<Profile user={data} />
)}
{orders.data?.length !== 0 && !orders.isLoading && orders.data?.map((order) => (
<OrderCard key={order.id} order={order} />
))}
</>
)
```
Для conditional rendering списков не используй прямые проверки длины массива:
```tsx
items?.length
items.length > 0
items.length !== 0
items.length === 0
!items.length
items && items.map(...)
```
Для уже типизированных массивов используй `isEmptyArray` и `isNonEmptyArray`:
```tsx
const ordersData = orders.data
{isNonEmptyArray(ordersData) && (
<OrdersList orders={ordersData} />
)}
{isEmptyArray(ordersData) && !orders.isLoading && (
<EmptyState />
)}
```
Для `unknown` или внешних API-данных используй `isArrayOf` с item guard:
```tsx
const ordersData = response.data
{isArrayOf(ordersData, isOrder) && (
<OrdersList orders={ordersData} />
)}
```
Перед проверкой выноси список в локальную переменную. Не дублируй длинный путь к данным внутри JSX:
```tsx
const itemsData = query.data
{isNonEmptyArray(itemsData) && itemsData.map((item) => (
<Card key={item.id} item={item} />
))}
```
Render-переменные используй только для крупных или повторяющихся веток:
```tsx
const ordersContent = isNonEmptyArray(ordersData) ? (
<OrdersList orders={ordersData} />
) : null
return (
<>
{ordersContent}
</>
)
```
Для подготовленных boolean-значений используй смысловые префиксы:
- `is*` - состояние или классификация: `isLoading`, `isProfileReady`.
- `has*` - наличие значения или содержимого: `hasOrders`, `hasError`.
- `can*` - возможность действия: `canSubmit`, `canEditProfile`.
- `should*` - UI-решение: `shouldShowEmptyState`, `shouldDisableSubmit`.
Для локальных данных используй имена с суффиксами `Data`, `Items`, `List`:
```tsx
const ordersData = orders.data
const cityItems = citySuggestions.data
```
### Wrapper-элементы и семантика
- Не добавляй wrapper-элемент только ради форматирования.
- Не добавляй wrapper, если он меняет DOM-структуру, CSS-поведение или доступность.
- Не меняй HTML-семантику или ARIA в задаче на чистое форматирование.
- Если задача требует выбрать семантический тег, ARIA или поведение формы, решай это как отдельную semantic/accessibility-задачу, а не как style-only правку.
```tsx
return (
<section aria-labelledby={titleId}>
<h2 id={titleId}>{title}</h2>
{children}
</section>
)
```
### Комментарии
- В HTML/JSX/TSX-разметке по умолчанию обходись без комментариев.
- Комментируй только хаки, внешние ограничения и неочевидные причины.
- JSX/TSX-комментарий оформляй только однострочно: `{/* Комментарий. */}`.
- HTML-комментарий оформляй только однострочно: `<!-- Комментарий. -->`.
- Не используй многострочные комментарии в HTML/JSX/TSX-разметке.
- Не оставляй комментарий в разметке, если можно дать блоку, компоненту или переменной понятное имя.
- Не комментируй очевидную структуру, семантику или назначение элемента.
### Чеклист HTML/JSX/TSX
- Атрибуты и props оформлены единообразно с локальным стилем файла.
- Строковые атрибуты используют двойные кавычки.
- Boolean-атрибуты записаны без лишнего `={true}`.
- Многострочный элемент читается без горизонтального скролла.
- В JSX/TSX-разметке нет тернарных выражений.
- JSX использует подготовленные локальные данные, `is*`, `has*`, `can*`, `should*` или простые predicate gates вместо составных runtime-условий в разметке.
- Conditional rendering списков использует `isEmptyArray`, `isNonEmptyArray` или `isArrayOf`, а не прямые `.length`-условия в JSX.
- Длинные пути к данным не дублируются внутри JSX: список выносится в локальную переменную до `return`.
- Render-переменные не создаются для простых условий без необходимости.
- Нет пустых, дублирующихся или случайных wrapper-элементов.
- Комментарии в HTML/JSX/TSX используются только для хаков, внешних ограничений и неочевидных причин.
## CSS
Этот канон описывает, как писать CSS без привязки к frontend-фреймворку, препроцессору или PostCSS-настройке. Он не описывает установку tooling, подключение плагинов и диагностику сборки.
### Базовый подход
- По умолчанию стили пишутся модульно: локальный UI-стиль живёт в CSS Module владельца.
- CSS Module принадлежит конкретному компоненту или модулю.
- Не импортируй CSS Module одного компонента или модуля в другой.
- Не выноси локальные классы компонента в global styles.
- Глобальные стили используй только для проектных основ: tokens, media, reset, typography, themes.
- Если стиль должен переиспользоваться, выноси переиспользуемую UI-сущность или token, а не общий CSS Module.
### CSS Modules
- Корневой класс CSS Module всегда называй `.root`.
- Обычные классы внутри CSS Module называй в `camelCase`.
- Не смешивай `camelCase`, BEM и `kebab-case` в одном CSS Module.
- Внешний `className` должен дополнять `.root`, а не заменять его.
- Модификаторы оформляй отдельными короткими классами с `_`: `._active`, `._disabled`, `._open`.
```css
.root {
display: flex;
}
.title {
color: var(--color-text);
}
.button {
opacity: 1;
&._disabled {
opacity: 0.5;
}
}
```
### Форматирование
- Используй 2 пробела. Не используй табы.
- В каждом CSS-правиле размещай одно свойство на строку.
- Между CSS-правилами верхнего уровня оставляй одну пустую строку.
- Перед каждым вложенным блоком оставляй одну пустую строку.
- Если проект не задаёт порядок, группируй свойства так: позиционирование, блочная модель, оформление, текст, прочее.
- Не меняй порядок свойств во всём файле без отдельной задачи на форматирование.
```css
.root {
position: relative;
display: flex;
width: 100%;
padding: var(--space-4);
border-radius: var(--radius-2);
background-color: var(--color-bg);
color: var(--color-text);
}
```
### Вложенность
- Не вкладывай селекторы друг в друга без необходимости.
- Разрешённая вложенность: `@media`, псевдоклассы, псевдоэлементы, modifier-классы.
- Не вкладывай элементы компонента внутрь `.root`; заводи отдельный класс.
- Не пиши каскад вида `.root .title`, если можно использовать локальный класс `.title`.
- Каждый вложенный блок отделяй пустой строкой от свойств и соседних вложенных блоков.
```css
.root {
display: flex;
color: var(--color-text);
&:hover {
color: var(--color-primary);
}
&::before {
content: '';
}
&._active {
opacity: 1;
}
@media (--md) {
display: grid;
}
}
.title {
color: var(--color-text);
}
```
Плохо:
```css
.root {
display: flex;
.title {
color: var(--color-text);
}
}
```
### Модификаторы
- Модификатор оформляй отдельным коротким классом с `_`: `._active`, `._disabled`, `._open`.
- Модификатор описывай через вложенность внутри базового класса: `&._active`.
- Не кодируй состояние через BEM-цепочки вроде `.button_active`.
- Не создавай отдельный modifier-файл.
- Не используй модификатор для самостоятельной сущности: если стиль можно назвать отдельным элементом, заведи отдельный класс.
```css
.root {
opacity: 1;
&._disabled {
pointer-events: none;
opacity: 0.5;
}
}
```
### Media queries
- Mobile First обязателен: базовое состояние описывает минимальный viewport без media query.
- Стили для больших viewport добавляй поверх базового состояния через `@media (--*)`.
- Запрещено писать desktop-first: сначала desktop-стили, потом откатывать их вниз через `max-width`.
- Запрещено игнорировать Mobile First ради удобства конкретного блока.
- По умолчанию используй custom media: `@media (--md)`, `@media (--lg)`, `@media (--h-md)`.
- Локальный `@media (min-width: ...)` допустим только как исключение для уникального порога, который нужен одному конкретному компоненту и не подходит для общей media-шкалы.
- Локальный `@media (min-width: ...)` не отменяет Mobile First: это тоже расширение вверх от базового состояния.
- У локального breakpoint обязательно оставляй короткий комментарий с причиной исключения.
- Если breakpoint совпадает с общей шкалой или может понадобиться повторно, используй существующий custom media или добавь новый в `media.css`.
- `@media` пиши внутри селектора, который изменяется.
- Не пиши `@media` верхнего уровня с набором селекторов внутри.
- Если шкалы media не хватает, расширяй `media.css` как проектный стандарт, а не добавляй локальный breakpoint.
```css
.root {
display: grid;
grid-template-columns: 1fr;
@media (--md) {
grid-template-columns: repeat(2, 1fr);
}
/* Уникальный порог, где карточки этого блока помещаются в 3 колонки. */
@media (min-width: 68.75rem) {
grid-template-columns: repeat(3, 1fr);
}
}
```
Плохо:
```css
@media (--md) {
.root {
display: flex;
}
.title {
font-size: 20px;
}
}
.root {
@media (min-width: 48rem) {
display: flex;
}
}
.card {
display: flex;
/* Плохо: desktop-first откат вниз вместо Mobile First расширения вверх. */
@media (max-width: 47.9375rem) {
display: block;
}
}
```
### media.css
- Все custom media объявляй в отдельном файле `media.css`.
- `media.css` содержит только `@custom-media`.
- Не объявляй custom media внутри CSS Modules.
- Не дублируй breakpoints локально в компонентах.
- Имена ширины пиши короткой шкалой `--xs` ... `--3xl`.
- Имена высоты пиши с префиксом `--h-`.
- Значения ширины пиши через `min-width`, кроме `--xs` как диапазона меньше `--sm`.
- Значения высоты пиши через `min-height`.
- Используй `rem` для значений media; пиксельный эквивалент указывай комментарием.
Базовый список media:
```css
/* Ширина: Mobile First, кроме --xs. */
@custom-media --xs (max-width: 35.9375rem); /* до 575px */
@custom-media --sm (min-width: 36rem); /* 576px */
@custom-media --md (min-width: 48rem); /* 768px */
@custom-media --lg (min-width: 62rem); /* 992px */
@custom-media --xl (min-width: 75rem); /* 1200px */
@custom-media --2xl (min-width: 88rem); /* 1408px */
@custom-media --3xl (min-width: 120rem); /* 1920px */
/* Высота. */
@custom-media --h-xs (min-height: 41.6875rem); /* 667px */
@custom-media --h-sm (min-height: 43.875rem); /* 702px */
@custom-media --h-md (min-height: 50.625rem); /* 810px */
@custom-media --h-lg (min-height: 56.25rem); /* 900px */
@custom-media --h-xl (min-height: 62.5rem); /* 1000px */
@custom-media --h-2xl (min-height: 68.75rem); /* 1100px */
@custom-media --h-3xl (min-height: 75rem); /* 1200px */
```
### Tokens и значения
- Цвета, радиусы, spacing и повторяемые смысловые значения используй через CSS variables.
- Локальное одноразовое значение можно оставить в CSS Module.
- Не выноси значение в token только ради форматирования.
- Не меняй token-систему в задаче на локальную CSS-правку.
- Не дублируй смысловое значение в нескольких CSS Modules, если оно уже является token.
```css
.root {
padding: var(--space-4);
border-radius: var(--radius-2);
background-color: var(--color-bg);
}
```
### Комментарии
- CSS-комментарий оформляй через `/* Комментарий. */`.
- Комментируй только неочевидные ограничения, hacks и причины.
- Не комментируй очевидные CSS-свойства.
- Удаляй устаревший комментарий при изменении поведения стиля.
### Чеклист CSS
- Локальный UI-стиль написан через CSS Module владельца.
- Корневой класс CSS Module называется `.root`.
- Классы CSS Module названы в `camelCase`, модификаторы - через `._modifier`.
- Одно CSS-свойство размещено на строку.
- Между правилами верхнего уровня и перед вложенными блоками есть пустая строка.
- Вложенность используется только для `@media`, псевдоклассов, псевдоэлементов и модификаторов.
- Mobile First соблюдён: базовое состояние написано без media query, расширения идут вверх через `@media (--*)`.
- Desktop-first и откаты вниз через `max-width` не используются.
- Breakpoints из общей шкалы используются через `@media (--*)`; локальные `min-width` есть только как обоснованные одноразовые исключения с комментарием причины.
- Все custom media объявлены в `media.css`.
- `media.css` содержит только `@custom-media`.
- Повторяемые смысловые значения оформлены через CSS variables.
- CSS-комментарии объясняют неочевидную причину, а не пересказывают свойства.