mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
1227 lines
62 KiB
Markdown
1227 lines
62 KiB
Markdown
|
|
---
|
|||
|
|
name: style-guide
|
|||
|
|
description: "Используй при создании, изменении, форматировании или ревью frontend-кода для соблюдения code style и style guide. Триггеры: JS, TS, JSX, TSX, HTML, CSS, .js, .ts, .jsx, .tsx, .module.css, React-компонент, props, стили, CSS Modules, JSDoc, документация, именование, импорты/экспорты, типизация, отформатировать по гайду, поправить кодстайл. НЕ используй для архитектуры SLM, выбора слоя/модуля/public API, генерации .templates, Next.js routing/data fetching/rendering, настройки PostCSS/tooling, backend-кода и вопросов без правки frontend-кода."
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
<!-- 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` |
|
|||
|
|
| Хуки | `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 не содержит форматирования незатронутого кода.
|
|||
|
|
- Отступы, длина строк и переносы не создают альтернативный стиль.
|
|||
|
|
- Имена соответствуют типу сущности и локальному стандарту проекта.
|
|||
|
|
- Комментарии объясняют причину или назначение, а не пересказывают код.
|
|||
|
|
|
|||
|
|
## 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
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Константы
|
|||
|
|
|
|||
|
|
- Документируй константу только если она является публичным контрактом, доменным ограничением, 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.
|
|||
|
|
- JS/TS-функции, классы, типы, интерфейсы, поля типов, enum, значения enum и константы-контракты документированы по правилам своего подраздела.
|
|||
|
|
|
|||
|
|
## React и TSX
|
|||
|
|
|
|||
|
|
Этот раздел описывает прикладную форму React UI-сущности после того, как архитектура уже определила её место: компонент внутри `ui/` или UI-модуль с корневым `.tsx`. Архитектурные границы компонентов, модулей, сегментов и public API определяет `slm-design`. Форму JSX-разметки дополнительно проверяй по разделу `HTML, JSX и TSX`.
|
|||
|
|
|
|||
|
|
### Базовая React UI-сущность
|
|||
|
|
|
|||
|
|
- Используй эту форму для базового React-компонента или UI-модуля с корневым компонентом.
|
|||
|
|
- Держи `.tsx`, props-типы, CSS Module и локальный `index.ts` в отдельных файлах.
|
|||
|
|
- Не объявляй props-типы внутри `.tsx`.
|
|||
|
|
- Не размещай CSS рядом с TSX-кодом: стили живут в `styles/{name}.module.css`.
|
|||
|
|
- Корневой CSS-класс всегда называется `.root`.
|
|||
|
|
- Компонент обязан иметь JSDoc-комментарий по React-шаблону.
|
|||
|
|
- Всё, что не является React-компонентом, документируй по правилам `Документирование JS/TS`.
|
|||
|
|
|
|||
|
|
### Структура файлов
|
|||
|
|
|
|||
|
|
- Базовая React UI-сущность живёт в собственной папке.
|
|||
|
|
- Файлы структуры обязательны для новой сущности: `.tsx`, `types/`, `styles/`, `index.ts`.
|
|||
|
|
- Имя файла props-типа строится как `{name}-props.type.ts`.
|
|||
|
|
- CSS Module строится как `{name}.module.css`.
|
|||
|
|
- `index.ts` экспортирует компонент и public props-тип.
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
user-status/
|
|||
|
|
├── styles/
|
|||
|
|
│ └── user-status.module.css
|
|||
|
|
├── types/
|
|||
|
|
│ └── user-status-props.type.ts
|
|||
|
|
├── user-status.tsx
|
|||
|
|
└── index.ts
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Типизация props
|
|||
|
|
|
|||
|
|
- Props-типы выноси в `types/{name}-props.type.ts`.
|
|||
|
|
- Собственные параметры компонента называй `{Name}Params`.
|
|||
|
|
- Атрибуты корневого HTML-элемента называй `RootAttrs`.
|
|||
|
|
- Итоговый тип props называй `{Name}Props`.
|
|||
|
|
- `RootAttrs` типизируй через `ComponentPropsWithoutRef<'tag'>`.
|
|||
|
|
- Исключай из `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 и импорт функции склейки классов.
|
|||
|
|
- Функцию склейки классов импортируй как `cl`: `import cl from 'clsx'`.
|
|||
|
|
- Компонент объявляй через `const` и именованный экспорт, если framework или локальный стандарт не требует другой формы.
|
|||
|
|
- Не используй `React.FC` по умолчанию.
|
|||
|
|
- Параметр компонента называй `props` и типизируй через `{Name}Props`.
|
|||
|
|
- Возвращаемый тип компонента не указывай: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата.
|
|||
|
|
- Деструктурируй props внутри тела компонента, а не в сигнатуре.
|
|||
|
|
- Из props обязательно выделяй `className` и `...rootAttrs`, если корневой элемент принимает HTML-атрибуты.
|
|||
|
|
- Прокидывай `rootAttrs` на корневой DOM-элемент.
|
|||
|
|
- Корневой элемент обязан получить `styles.root` первым CSS-классом.
|
|||
|
|
- Внешний `className` добавляй последним аргументом в `cl(...)`.
|
|||
|
|
- Не создавай вложенный компонент внутри 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`.
|
|||
|
|
- Корневой CSS-класс всегда называй `.root`.
|
|||
|
|
- `.root` описывает реальный корневой DOM-элемент компонента.
|
|||
|
|
- Дополнительные классы описывай рядом с `.root` и подключай через `styles.*`.
|
|||
|
|
- Не подменяй `.root` внешним `className`: внешний класс только дополняет `styles.root`.
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
.root {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 6px;
|
|||
|
|
color: var(--color-text-muted);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Локальный экспорт
|
|||
|
|
|
|||
|
|
- В `index.ts` экспортируй компонент и public props-тип.
|
|||
|
|
- Компонент экспортируй обычным named export.
|
|||
|
|
- Props экспортируй через `export type`.
|
|||
|
|
- Не экспортируй `Params` и `RootAttrs` наружу без необходимости.
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
export { UserStatus } from './user-status'
|
|||
|
|
export type { UserStatusProps } from './types/user-status-props.type'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Документирование компонентов
|
|||
|
|
|
|||
|
|
- Каждый React-компонент должен иметь многострочный JSDoc-комментарий.
|
|||
|
|
- Комментарий ставь непосредственно перед объявлением компонента.
|
|||
|
|
- Компонент документируй по React-шаблону, а не по шаблону функции из `JS/TS`.
|
|||
|
|
- Компонент описывает назначение и сценарии применения.
|
|||
|
|
- Комментарий должен помогать понять, когда использовать компонент, без чтения реализации.
|
|||
|
|
- В `Используется для` указывай только реальные сценарии применения.
|
|||
|
|
- Добавляй минимум один сценарий; верхнего ограничения по количеству сценариев нет.
|
|||
|
|
- Не добавляй сценарии ради количества.
|
|||
|
|
- Не описывай реализацию вида `рендерит 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
|
|||
|
|
|
|||
|
|
- Базовая React UI-сущность имеет `.tsx`, `types/`, `styles/` и `index.ts`.
|
|||
|
|
- Props вынесены в `types/{name}-props.type.ts` и собраны из `{Name}Params`, `RootAttrs`, `{Name}Props`.
|
|||
|
|
- Props-типы и поля props документированы по правилам `JS/TS`.
|
|||
|
|
- Компонент объявлен через `const`, принимает `props` и деструктурирует их внутри тела.
|
|||
|
|
- Компонент не использует `React.FC` по умолчанию и не указывает возвращаемый тип без необходимости.
|
|||
|
|
- Корневой DOM-элемент получает `{...rootAttrs}` и `className={cl(styles.root, ..., className)}`.
|
|||
|
|
- CSS Module лежит в `styles/{name}.module.css`, корневой класс называется `.root`.
|
|||
|
|
- `index.ts` экспортирует компонент и props-тип.
|
|||
|
|
- Каждый React-компонент имеет комментарий по 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-комментарии объясняют неочевидную причину, а не пересказывают свойства.
|