Files
slm-design/.opencode/skills/style-guide/SKILL.md
2026-08-01 09:31:08 +03:00

62 KiB
Raw Blame History

name, description
name description
style-guide Используй при создании, изменении, форматировании или ревью 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-кода.

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.

Строки

  • Для обычных строк используй одинарные кавычки.
  • Строки в обратных кавычках используй только для интерполяции ${...} или многострочного текста.
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, если экспортируется только тип.
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.
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.
const getName = (user?: { name: string }) => {
  if (!user) {
    return 'Гость';
  }

  return user.name;
};

Булевые значения

  • Булевые значения начинай с is, has, can или should.
  • Не используй нейтральные имена вроде ready, access, submit, если по имени неясно, что это boolean.
const isReady = true;
const hasAccess = false;
const canSubmit = true;
const shouldRedirect = false;

События и callback

  • Обработчики внутри кода называй через handle*.
  • Callback-параметры и callback-свойства называй через on*.
const handleSubmit = () => {
  // ...
};

type FormOptions = {
  onSubmit: () => void;
};

TypeScript

  • Указывай типы для параметров функций и компонентов.
  • Для публичных функций указывай возвращаемый тип.
  • Не полагайся на неявный вывод типов для публичных API и важных контрактов.
  • Предпочитай type для описания сущностей и interface для расширяемых контрактов.
  • Избегай any и unknown без необходимости.
  • Не используй // @ts-ignore, кроме крайних случаев с явным комментарием причины.
  • Если нужно временно подавить ошибку TypeScript, предпочитай // @ts-expect-error с объяснением причины.
/**
 * Форматирует цену с символом валюты.
 */
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 в одной области без причины.
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 используй на границах с внешними данными и обязательно сужай перед использованием.
const parseName = (value: unknown): string => {
  if (typeof value === 'string') {
    return value;
  }

  return '';
};

Плохо:

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.

Минимальная структура библиотеки:

shared/lib/value-predicates/
├── index.ts
└── value-predicates.ts
import { hasOwn, isArrayOf, isNonEmptyArray, isRecord, isString } from 'shared/lib/value-predicates'

Predicates применяй, когда нужно проверить:

  • значение определено или отсутствует;
  • значение является string, number или boolean;
  • строка является непустой;
  • массив пустой или непустой;
  • unknown-значение является массивом элементов нужной формы;
  • unknown-значение является объектом-записью;
  • значение входит в список допустимых литералов.

Для определения пустого или непустого списка не используй прямые проверки длины массива:

items?.length
items.length > 0
items.length !== 0
items.length === 0
!items.length

Используй predicates:

isEmptyArray(items)
isNonEmptyArray(items)

Для unknown-данных и внешних API-ответов не используй Array.isArray(value) как проверку формы элементов. Используй isArrayOf с item guard:

if (isArrayOf(value, isOrder)) {
  // value: Order[]
}

Для object-like unknown используй isRecord и hasOwn перед чтением полей:

const isOrder = (value: unknown): value is Order => {
  return isRecord(value) && hasOwn(value, 'id') && isString(value.id)
}

Для literal union используй isOneOf:

const statuses = ['draft', 'published'] as const

if (isOneOf(value, statuses)) {
  // value: 'draft' | 'published'
}

Прямое обращение к .length допустимо, когда нужен именно числовой размер:

const count = items.length

if (password.length < 8) {
  return 'Минимум 8 символов'
}

for (let index = 0; index < items.length; index += 1) {
  // ...
}

Прямой map допустим, если массив заранее нормализован:

const items = data ?? []

return items.map((item) => mapItem(item))

Type assertions

  • Не используй as для обхода TypeScript без проверки данных.
  • Допускай as const для литеральных конфигураций и enum-like объектов.
  • Type assertion на границе внешних данных должен сопровождаться проверкой, схемой валидации или явной причиной.
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-комментарий.
/**
 * <Что делает сущность в 1 строке>.
 *
 * <Опционально: описание сложной механики или важных нюансов>.
 */

Функции

  • Каждая именованная функция должна иметь многострочный JSDoc-комментарий.
  • Под правило попадают function name(), const name = () => {}, const name = function () {} и именованные обработчики вроде const handleSubmit = () => {}.
  • Комментарий функции описывает действие, результат, побочный эффект или важное ограничение.
/**
 * Рекурсивно собирает дерево категорий из плоского списка.
 *
 * Группирует элементы по parentId, начиная с корневых категорий.
 * Категории без родителя попадают в корень дерева.
 */
export const buildCategoryTree = (categories: Category[]): CategoryTree[] => {
  // ...
}

Классы

  • Каждый класс должен иметь многострочный JSDoc-комментарий.
  • Комментарий класса описывает роль класса и границу ответственности.
  • Не перечисляй методы класса, если их назначение понятно из public API.
/**
 * Клиент для работы с заказами через REST API.
 *
 * Инкапсулирует маршруты, сериализацию параметров и обработку ответа.
 */
export class OrdersClient {
  // ...
}

Типы и интерфейсы

  • Каждый type и interface должен иметь многострочный JSDoc-комментарий.
  • Комментарий типа или интерфейса описывает контракт: модель, DTO, props, config, состояние или внешний формат.
  • Каждое поле type или interface должно иметь JSDoc-комментарий /** ... */ непосредственно над полем.
  • Комментарий поля описывает смысл значения, доменное ограничение, формат или сценарий использования.
  • Не объединяй одним комментарием несколько полей.
  • Не отделяй поля пустой строкой только из-за JSDoc-комментария.
  • Пустую строку между полями добавляй только для смыслового разделения групп полей.
  • Если поле кажется очевидным, всё равно добавь короткое описание без пересказа имени.
/**
 * Фильтры списка заказов.
 */
export type OrderFilters = {
  /** Идентификатор пользователя-владельца заказов. */
  userId?: string
  /** Статус заказа для фильтрации выдачи. */
  status?: OrderStatus
}

Константы

  • Документируй константу только если она является публичным контрактом, доменным ограничением, magic value или переиспользуемой конфигурацией.
  • Обычные локальные константы не документируй.
/**
 * Максимальное количество заказов на одной странице выдачи.
 *
 * Значение синхронизировано с ограничением backend API.
 */
export const MAX_ORDERS_PAGE_SIZE = 100

Enum

  • Каждый enum должен иметь многострочный JSDoc-комментарий.
  • Комментарий enum описывает назначение набора значений.
  • Каждое значение enum должно иметь JSDoc-комментарий /** ... */ непосредственно над значением.
  • Комментарий значения enum описывает смысл значения или состояние, которое оно обозначает.
  • Не отделяй значения enum пустой строкой только из-за JSDoc-комментария.
  • Не оставляй значение enum без комментария, даже если оно кажется очевидным.
/**
 * Состояние оплаты заказа.
 */
export enum PaymentStatus {
  /** Оплата создана, но ещё не подтверждена провайдером. */
  PENDING = 'pending',
  /** Оплата успешно подтверждена провайдером. */
  PAID = 'paid',
  /** Оплата отклонена или завершилась ошибкой. */
  FAILED = 'failed'
}

Что не документировать

  • Не документируй обычные переменные и inline callback внутри вызовов вроде useEffect, map, filter, reduce, forEach, then или обработчиков API.
  • Если inline callback требует пояснения, вынеси его в именованную функцию и задокументируй объявление.

Плохо

/**
 * @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-тип.
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.
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.
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.
.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 наружу без необходимости.
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.

Шаблон:

/**
 * <Назначение компонента в 1 строке>.
 *
 * Используется для:
 *  - <сценарий 1>
 *  - <сценарий 2, если есть>
 */

Плохо:

/**
 * Рендерит 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.
<button
  type="button"
  disabled={isSaving}
  onClick={handleSave}
>
  Сохранить
</button>

Формат элемента

  • Короткий элемент с небольшим числом атрибутов или props можно оставлять в одну строку.
  • Если атрибутов или props много, размещай каждый на отдельной строке.
  • Закрывающую скобку многострочного элемента размещай на отдельной строке.
  • Если элемент содержит вложенные элементы, размещай открывающий и закрывающий теги на отдельных строках.
  • Не смешивай в одном участке разметки разные стили записи props без причины.
<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.
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 />
    )}
  </>
)

Плохо:

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 списков не используй прямые проверки длины массива:

items?.length
items.length > 0
items.length !== 0
items.length === 0
!items.length
items && items.map(...)

Для уже типизированных массивов используй isEmptyArray и isNonEmptyArray:

const ordersData = orders.data

{isNonEmptyArray(ordersData) && (
  <OrdersList orders={ordersData} />
)}

{isEmptyArray(ordersData) && !orders.isLoading && (
  <EmptyState />
)}

Для unknown или внешних API-данных используй isArrayOf с item guard:

const ordersData = response.data

{isArrayOf(ordersData, isOrder) && (
  <OrdersList orders={ordersData} />
)}

Перед проверкой выноси список в локальную переменную. Не дублируй длинный путь к данным внутри JSX:

const itemsData = query.data

{isNonEmptyArray(itemsData) && itemsData.map((item) => (
  <Card key={item.id} item={item} />
))}

Render-переменные используй только для крупных или повторяющихся веток:

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:

const ordersData = orders.data
const cityItems = citySuggestions.data

Wrapper-элементы и семантика

  • Не добавляй wrapper-элемент только ради форматирования.
  • Не добавляй wrapper, если он меняет DOM-структуру, CSS-поведение или доступность.
  • Не меняй HTML-семантику или ARIA в задаче на чистое форматирование.
  • Если задача требует выбрать семантический тег, ARIA или поведение формы, решай это как отдельную semantic/accessibility-задачу, а не как style-only правку.
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.
.root {
  display: flex;
}

.title {
  color: var(--color-text);
}

.button {
  opacity: 1;

  &._disabled {
    opacity: 0.5;
  }
}

Форматирование

  • Используй 2 пробела. Не используй табы.
  • В каждом 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.
  • Каждый вложенный блок отделяй пустой строкой от свойств и соседних вложенных блоков.
.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);
}

Плохо:

.root {
  display: flex;

  .title {
    color: var(--color-text);
  }
}

Модификаторы

  • Модификатор оформляй отдельным коротким классом с _: ._active, ._disabled, ._open.
  • Модификатор описывай через вложенность внутри базового класса: &._active.
  • Не кодируй состояние через BEM-цепочки вроде .button_active.
  • Не создавай отдельный modifier-файл.
  • Не используй модификатор для самостоятельной сущности: если стиль можно назвать отдельным элементом, заведи отдельный класс.
.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.
.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);
  }
}

Плохо:

@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:

/* Ширина: 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.
.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-комментарии объясняют неочевидную причину, а не пересказывают свойства.