--- 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-кода." --- # 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..ts` или `name..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 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; set(key: string, value: string): Promise; } ``` ### 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, '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 ( {label} ) } ``` ### 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 ``` ### Формат элемента - Короткий элемент с небольшим числом атрибутов или props можно оставлять в одну строку. - Если атрибутов или props много, размещай каждый на отдельной строке. - Закрывающую скобку многострочного элемента размещай на отдельной строке. - Если элемент содержит вложенные элементы, размещай открывающий и закрывающий теги на отдельных строках. - Не смешивай в одном участке разметки разные стили записи props без причины. ```tsx ``` ### Условия и значения в разметке - Не усложняй 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 ( <> {isDefined(profileUser) && ( )} {isNonEmptyArray(ordersData) && ordersData.map((order) => ( ))} {shouldShowEmptyState && ( )} ) ``` Плохо: ```tsx return ( <> {data && !currentUser.error && ( )} {orders.data?.length !== 0 && !orders.isLoading && orders.data?.map((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) && ( )} {isEmptyArray(ordersData) && !orders.isLoading && ( )} ``` Для `unknown` или внешних API-данных используй `isArrayOf` с item guard: ```tsx const ordersData = response.data {isArrayOf(ordersData, isOrder) && ( )} ``` Перед проверкой выноси список в локальную переменную. Не дублируй длинный путь к данным внутри JSX: ```tsx const itemsData = query.data {isNonEmptyArray(itemsData) && itemsData.map((item) => ( ))} ``` Render-переменные используй только для крупных или повторяющихся веток: ```tsx const ordersContent = isNonEmptyArray(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 (

{title}

{children}
) ``` ### Комментарии - В 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-комментарии объясняют неочевидную причину, а не пересказывают свойства.