Files

1331 lines
72 KiB
Markdown
Raw Permalink Normal View History

2026-08-01 09:31:08 +03:00
---
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
2026-08-01 09:31:08 +03:00
---
<!-- 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` |
2026-08-01 09:31:08 +03:00
| Хуки | `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-единицей модуля.
- Компонентные каталоги не создают рекурсивную файловую иерархию.
2026-08-01 09:31:08 +03:00
## 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'
```
2026-08-01 09:31:08 +03:00
#### Константы
- Документируй константу только если она является публичным контрактом, доменным ограничением, 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.
2026-08-01 09:31:08 +03:00
- JS/TS-функции, классы, типы, интерфейсы, поля типов, enum, значения enum и константы-контракты документированы по правилам своего подраздела.
## React и TSX
Этот раздел описывает прикладную форму React framework-компонента после того, как архитектура уже определила его владельца и место. Правила распространяются на визуальные компоненты, Providers, Guards и Error Boundaries. Архитектурные границы компонентов, модулей, сегментов и public API определяет `slm-design`. Форму JSX-разметки дополнительно проверяй по разделу `HTML, JSX и TSX`.
2026-08-01 09:31:08 +03:00
### Обязательный обвес framework-компонента
2026-08-01 09:31:08 +03:00
- Используй эту форму для каждого React framework-компонента, включая Provider, Guard и Error Boundary.
- Каждый вложенный framework-компонент размещай в отдельной папке согласно разделу `Физическая структура SLM`.
2026-08-01 09:31:08 +03:00
- Держи `.tsx`, props-типы, CSS Module и локальный `index.ts` в отдельных файлах.
- Файлы структуры обязательны даже для невизуального компонента без собственного DOM: `.tsx`, `types/`, `styles/`, `index.ts`.
2026-08-01 09:31:08 +03:00
- Не объявляй props-типы внутри `.tsx`.
- Не размещай CSS рядом с TSX-кодом: стили живут в `styles/{name}.module.css`.
- Для компонента с собственным DOM корневой CSS-класс называется `.root`.
- Для невизуального компонента создай пустой CSS Module, но не импортируй его до появления стилизуемого DOM.
- Не добавляй Provider, Guard или другой невизуальный компонент DOM-wrapper только ради использования CSS Module.
2026-08-01 09:31:08 +03:00
- Компонент обязан иметь JSDoc-комментарий по React-шаблону.
- Всё, что не является React-компонентом, документируй по правилам `Документирование JS/TS`.
### Структура файлов
- Вложенный framework-компонент живёт в собственной папке.
2026-08-01 09:31:08 +03:00
- Имя файла props-типа строится как `{name}-props.type.ts`.
- CSS Module строится как `{name}.module.css`.
- Локальный `index.ts` экспортирует компонент и props-тип для использования внутри модуля.
- Единственное исключение из собственной папки — главный framework-файл модуля, включая главный Provider.
- Для главного framework-файла папкой компонента является корень модуля: `styles/` и `types/` создаются непосредственно в нём.
- Корневой `index.ts` остаётся публичным фасетом модуля и экспортирует компонент только через фасет, разрешённый архитектурой и средой выполнения.
2026-08-01 09:31:08 +03:00
```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
```
2026-08-01 09:31:08 +03:00
### Типизация props
- Props-типы выноси в `types/{name}-props.type.ts`.
- Собственные параметры компонента называй `{Name}Params`.
- Атрибуты собственного корневого HTML-элемента называй `RootAttrs`.
2026-08-01 09:31:08 +03:00
- Итоговый тип props называй `{Name}Props`.
- Для компонента с собственным DOM типизируй `RootAttrs` через `ComponentPropsWithoutRef<'tag'>`.
- Для Provider или другого компонента без собственного DOM не создавай фиктивный `RootAttrs`.
2026-08-01 09:31:08 +03:00
- Исключай из `RootAttrs` атрибуты, которыми компонент управляет сам: например `children`, если компонент рендерит собственный контент.
- Если собственный параметр конфликтует с HTML-атрибутом, исключай этот атрибут через `Omit`.
- Документируй все объявленные `Params`, `RootAttrs`, `Props` и каждое поле props по правилам `JS/TS`.
2026-08-01 09:31:08 +03:00
```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.
2026-08-01 09:31:08 +03:00
- Функцию склейки классов импортируй как `cl`: `import cl from 'clsx'`.
- Функциональный компонент объявляй через `const` и именованный экспорт, если framework или локальный стандарт не требует другой формы.
- Нативный React Error Boundary разрешено объявлять классом, потому что React не предоставляет функциональный lifecycle для перехвата ошибок дочернего дерева.
2026-08-01 09:31:08 +03:00
- Не используй `React.FC` по умолчанию.
- Параметр функционального компонента называй `props` и типизируй через `{Name}Props`.
2026-08-01 09:31:08 +03:00
- Возвращаемый тип компонента не указывай: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата.
- Деструктурируй props функционального компонента внутри тела, а не в сигнатуре.
- Из props выделяй `className` и `...rootAttrs`, если компонент имеет собственный корневой DOM-элемент и принимает его HTML-атрибуты.
- Прокидывай `rootAttrs` на собственный корневой DOM-элемент.
- Собственный корневой DOM-элемент обязан получить `styles.root` первым CSS-классом.
2026-08-01 09:31:08 +03:00
- Внешний `className` добавляй последним аргументом в `cl(...)`.
- Не добавляй невизуальному компоненту DOM-элемент, `className` или `rootAttrs`, если они не нужны его контракту.
2026-08-01 09:31:08 +03:00
- Не создавай вложенный компонент внутри 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`.
2026-08-01 09:31:08 +03:00
- `.root` описывает реальный корневой DOM-элемент компонента.
- Дополнительные классы описывай рядом с `.root` и подключай через `styles.*`.
- Не подменяй `.root` внешним `className`: внешний класс только дополняет `styles.root`.
- CSS Module невизуального компонента оставляй пустым и не импортируй, пока у компонента не появится собственный стилизуемый DOM.
- Не создавай DOM-wrapper только для применения `.root`.
2026-08-01 09:31:08 +03:00
```css
.root {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--color-text-muted);
}
```
### Локальный экспорт
- В локальном `index.ts` экспортируй компонент и props-тип.
2026-08-01 09:31:08 +03:00
- Компонент экспортируй обычным named export.
- Props экспортируй через `export type`.
- Не экспортируй `Params` и `RootAttrs` наружу без необходимости.
- Для корневого framework-файла используй публичный фасет модуля и не обходи его требования к среде выполнения.
2026-08-01 09:31:08 +03:00
```ts
export { UserStatus } from './user-status'
export type { UserStatusProps } from './types/user-status-props.type'
```
### Документирование компонентов
- Каждый React framework-компонент, включая Provider, Guard и Error Boundary, должен иметь многострочный JSDoc-комментарий.
2026-08-01 09:31:08 +03:00
- Комментарий ставь непосредственно перед объявлением компонента.
- Компонент документируй по React-шаблону, а не по шаблону функции из `JS/TS`.
- Нативный Error Boundary, объявленный классом, также документируй по React-шаблону как компонент.
2026-08-01 09:31:08 +03:00
- Компонент описывает назначение и сценарии применения.
- Комментарий должен помогать понять, когда использовать компонент, без чтения реализации.
- В `Используется для` указывай только реальные сценарии применения.
- Добавляй минимум один сценарий; верхнего ограничения по количеству сценариев нет.
- Не добавляй сценарии ради количества.
- Не описывай реализацию вида `рендерит 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.
2026-08-01 09:31:08 +03:00
- Props-типы и поля props документированы по правилам `JS/TS`.
- Функциональный компонент объявлен через `const`, принимает `props` и деструктурирует их внутри тела; нативный Error Boundary может быть классом.
2026-08-01 09:31:08 +03:00
- Компонент не использует `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-шаблону с назначением и сценариями применения.
2026-08-01 09:31:08 +03:00
- 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-комментарии объясняют неочевидную причину, а не пересказывают свойства.