mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 15:30:16 +03:00
1331 lines
72 KiB
Markdown
1331 lines
72 KiB
Markdown
---
|
||
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-комментарии объясняют неочевидную причину, а не пересказывают свойства.
|