From f2358da397d8bcd3b960c60053aa42ae558b415b Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Thu, 30 Apr 2026 19:32:10 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D0=B9=D0=BB=D0=B3=D0=B0=D0=B9?= =?UTF-8?q?=D0=B4=20nextjs-style-guide=20=D0=B2=20=D1=80=D0=B5=D0=BF=D0=BE?= =?UTF-8?q?=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена документация SLM-архитектуры, базовых правил и прикладных разделов - Добавлены разделы: стили, SVG-спрайты, шаблоны генерации, PostCSS, REST, Realtime - Удалены устаревшие файлы (спрайты, скрипты, стили из app/) --- ai/nextjs-style-guide/DEVELOP.md | 103 +++++++ ai/nextjs-style-guide/MAP.md | 66 +++++ ai/nextjs-style-guide/VERSION | 2 + ai/nextjs-style-guide/applied/aliases.md | 77 +++++ ai/nextjs-style-guide/applied/biome.md | 81 ++++++ ai/nextjs-style-guide/applied/component.md | 201 +++++++++++++ ai/nextjs-style-guide/applied/fonts.md | 128 +++++++++ ai/nextjs-style-guide/applied/images.md | 95 ++++++ ai/nextjs-style-guide/applied/localization.md | 81 ++++++ ai/nextjs-style-guide/applied/module.md | 162 +++++++++++ ai/nextjs-style-guide/applied/page-level.md | 186 ++++++++++++ ai/nextjs-style-guide/applied/postcss.md | 70 +++++ .../applied/project-structure.md | 101 +++++++ .../nextjs-style-guide/applied/stores.md | 0 .../applied/styles/styles-setup.md | 176 ++++++++++++ .../applied/styles/styles-usage.md | 271 ++++++++++++++++++ .../applied/svg-sprites/svg-sprites-intro.md | 31 ++ .../applied/svg-sprites/svg-sprites-setup.md | 132 +++++++++ .../applied/svg-sprites/svg-sprites-usage.md | 56 ++++ .../applied/templates/templates-create.md | 91 ++++++ .../applied/templates/templates-intro.md | 32 +++ .../applied/templates/templates-setup.md | 44 +++ .../applied/templates/templates-usage.md | 39 +++ ai/nextjs-style-guide/applied/vscode.md | 88 ++++++ .../basics/architecture/index.md | 100 +++++++ .../basics/architecture/reference/layers.md | 253 ++++++++++++++++ .../basics/architecture/reference/modules.md | 165 +++++++++++ .../basics/architecture/reference/segments.md | 159 ++++++++++ ai/nextjs-style-guide/basics/code-style.md | 153 ++++++++++ ai/nextjs-style-guide/basics/documentation.md | 134 +++++++++ ai/nextjs-style-guide/basics/naming.md | 146 ++++++++++ ai/nextjs-style-guide/basics/tech-stack.md | 42 +++ ai/nextjs-style-guide/basics/typing.md | 57 ++++ .../creating-project/from-template.md | 51 ++++ .../creating-project/manual.md | 90 ++++++ .../creating-project/nextjs.md | 112 ++++++++ ai/nextjs-style-guide/data/index.md | 60 ++++ ai/nextjs-style-guide/data/realtime.md | 79 +++++ .../data/rest/clients/auto.md | 193 +++++++++++++ .../data/rest/clients/hooks.md | 206 +++++++++++++ .../data/rest/clients/index.md | 75 +++++ .../data/rest/clients/manual.md | 187 ++++++++++++ ai/nextjs-style-guide/data/rest/index.md | 74 +++++ .../rest/strategies/business-composition.md | 121 ++++++++ .../data/rest/strategies/client-get-hook.md | 89 ++++++ .../strategies/client-hooks-initial-data.md | 109 +++++++ .../data/rest/strategies/index.md | 100 +++++++ .../strategies/parallel-server-requests.md | 82 ++++++ .../data/rest/strategies/pass-promise-down.md | 62 ++++ .../data/rest/strategies/server-await.md | 88 ++++++ ai/nextjs-style-guide/workflow.md | 8 + public/img/sprites/icons/sprite.stack.html | 161 ----------- public/img/sprites/icons/sprite.stack.svg | 1 - scripts/create-svg-sprite.js | 126 -------- src/app/providers/index.ts | 1 - src/app/providers/mantine-provider.tsx | 11 - src/app/styles/index.css | 3 - src/app/styles/media.css | 6 - src/app/styles/reset.css | 28 -- src/app/styles/variables.css | 35 --- 60 files changed, 5308 insertions(+), 372 deletions(-) create mode 100644 ai/nextjs-style-guide/DEVELOP.md create mode 100644 ai/nextjs-style-guide/MAP.md create mode 100644 ai/nextjs-style-guide/VERSION create mode 100644 ai/nextjs-style-guide/applied/aliases.md create mode 100644 ai/nextjs-style-guide/applied/biome.md create mode 100644 ai/nextjs-style-guide/applied/component.md create mode 100644 ai/nextjs-style-guide/applied/fonts.md create mode 100644 ai/nextjs-style-guide/applied/images.md create mode 100644 ai/nextjs-style-guide/applied/localization.md create mode 100644 ai/nextjs-style-guide/applied/module.md create mode 100644 ai/nextjs-style-guide/applied/page-level.md create mode 100644 ai/nextjs-style-guide/applied/postcss.md create mode 100644 ai/nextjs-style-guide/applied/project-structure.md rename src/infrastructure/.gitkeep => ai/nextjs-style-guide/applied/stores.md (100%) create mode 100644 ai/nextjs-style-guide/applied/styles/styles-setup.md create mode 100644 ai/nextjs-style-guide/applied/styles/styles-usage.md create mode 100644 ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-intro.md create mode 100644 ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-setup.md create mode 100644 ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-usage.md create mode 100644 ai/nextjs-style-guide/applied/templates/templates-create.md create mode 100644 ai/nextjs-style-guide/applied/templates/templates-intro.md create mode 100644 ai/nextjs-style-guide/applied/templates/templates-setup.md create mode 100644 ai/nextjs-style-guide/applied/templates/templates-usage.md create mode 100644 ai/nextjs-style-guide/applied/vscode.md create mode 100644 ai/nextjs-style-guide/basics/architecture/index.md create mode 100644 ai/nextjs-style-guide/basics/architecture/reference/layers.md create mode 100644 ai/nextjs-style-guide/basics/architecture/reference/modules.md create mode 100644 ai/nextjs-style-guide/basics/architecture/reference/segments.md create mode 100644 ai/nextjs-style-guide/basics/code-style.md create mode 100644 ai/nextjs-style-guide/basics/documentation.md create mode 100644 ai/nextjs-style-guide/basics/naming.md create mode 100644 ai/nextjs-style-guide/basics/tech-stack.md create mode 100644 ai/nextjs-style-guide/basics/typing.md create mode 100644 ai/nextjs-style-guide/creating-project/from-template.md create mode 100644 ai/nextjs-style-guide/creating-project/manual.md create mode 100644 ai/nextjs-style-guide/creating-project/nextjs.md create mode 100644 ai/nextjs-style-guide/data/index.md create mode 100644 ai/nextjs-style-guide/data/realtime.md create mode 100644 ai/nextjs-style-guide/data/rest/clients/auto.md create mode 100644 ai/nextjs-style-guide/data/rest/clients/hooks.md create mode 100644 ai/nextjs-style-guide/data/rest/clients/index.md create mode 100644 ai/nextjs-style-guide/data/rest/clients/manual.md create mode 100644 ai/nextjs-style-guide/data/rest/index.md create mode 100644 ai/nextjs-style-guide/data/rest/strategies/business-composition.md create mode 100644 ai/nextjs-style-guide/data/rest/strategies/client-get-hook.md create mode 100644 ai/nextjs-style-guide/data/rest/strategies/client-hooks-initial-data.md create mode 100644 ai/nextjs-style-guide/data/rest/strategies/index.md create mode 100644 ai/nextjs-style-guide/data/rest/strategies/parallel-server-requests.md create mode 100644 ai/nextjs-style-guide/data/rest/strategies/pass-promise-down.md create mode 100644 ai/nextjs-style-guide/data/rest/strategies/server-await.md create mode 100644 ai/nextjs-style-guide/workflow.md delete mode 100644 public/img/sprites/icons/sprite.stack.html delete mode 100644 public/img/sprites/icons/sprite.stack.svg delete mode 100644 scripts/create-svg-sprite.js delete mode 100644 src/app/providers/index.ts delete mode 100644 src/app/providers/mantine-provider.tsx delete mode 100644 src/app/styles/index.css delete mode 100644 src/app/styles/media.css delete mode 100644 src/app/styles/reset.css delete mode 100644 src/app/styles/variables.css diff --git a/ai/nextjs-style-guide/DEVELOP.md b/ai/nextjs-style-guide/DEVELOP.md new file mode 100644 index 0000000..265090d --- /dev/null +++ b/ai/nextjs-style-guide/DEVELOP.md @@ -0,0 +1,103 @@ +--- +title: Гид для агента +description: Что AI-агент обязан прочитать перед началом работы, а что — по задаче. +--- + +# Обязательное чтение перед началом работы + +Этот документ определяет **строгий порядок действий агента перед выполнением любых задач**. + +## Общее правило + +Перед началом работы над **любой задачей** агент **обязан ознакомиться с базовой документацией проекта**. + +Нарушение этого порядка считается ошибкой. + +--- + +## Порядок обязательного чтения + +Агент должен читать документацию **строго в следующем порядке**: + +### 1. Архитектура (КРИТИЧЕСКИ ВАЖНО) + +* [Архитектура: Обзор](./basics/architecture/index.md) +* [Архитектура: Слои](./basics/architecture/reference/layers.md) +* [Архитектура: Модули](./basics/architecture/reference/modules.md) +* [Архитектура: Сегменты](./basics/architecture/reference/segments.md) + +**Архитектура — это самое важное в проекте.** + +Агент обязан: + +* строго понимать архитектурный подход (SLM) +* соблюдать архитектуру **на 100% без отклонений** +* не предлагать решений, нарушающих архитектурные принципы +* не упрощать архитектуру даже ради скорости выполнения задачи + +Любое нарушение архитектуры недопустимо. + +--- + +### 2. Базовые правила + +После архитектуры необходимо изучить: + +* [Технологии и библиотеки](./basics/tech-stack.md) +* [Именование](./basics/naming.md) +* [Стиль кода](./basics/code-style.md) +* [Документирование](./basics/documentation.md) +* [Типизация](./basics/typing.md) + +Агент обязан применять эти правила во всех решениях. + +--- + +## Использование карты документации + +Для поиска дополнительных сведений агент должен использовать: + +* [MAP.md](./MAP.md) + +MAP.md содержит ссылки на все прикладные и вспомогательные разделы. + +Агент может: + +* переходить к нужным разделам через MAP.md +* уточнять детали реализации +* искать примеры и частные случаи + +--- + +## Запрещено + +Агенту запрещено: + +* начинать выполнение задачи без изучения архитектуры +* игнорировать базовые правила +* принимать решения, противоречащие архитектуре +* придумывать собственные подходы, если они не описаны в документации + +--- + +## Ожидаемое поведение агента + +Перед выполнением задачи агент должен: + +1. Изучить архитектуру +2. Изучить базовые правила +3. При необходимости открыть MAP.md и найти релевантные разделы +4. Только после этого приступать к решению задачи + +--- + +## Приоритеты + +При принятии решений агент должен руководствоваться следующим приоритетом: + +1. **Архитектура** +2. Базовые правила +3. Документация из MAP.md +4. Задача пользователя + +Если задача противоречит архитектуре — задача должна быть переосмыслена, а не выполнена напрямую. diff --git a/ai/nextjs-style-guide/MAP.md b/ai/nextjs-style-guide/MAP.md new file mode 100644 index 0000000..2f022c8 --- /dev/null +++ b/ai/nextjs-style-guide/MAP.md @@ -0,0 +1,66 @@ +# Карта документации + +Список всех разделов архива с относительными ссылками. Точка входа +— `DEVELOP.md` рядом с этим файлом. + +## Подсказки + +- [Подсказки](./workflow.md) — Короткие ответы на типовые вопросы и решения для спорных ситуаций. + +## Базовые правила + +- [Технологии и библиотеки](./basics/tech-stack.md) — Какие библиотеки и инструменты используются в проекте. +- [Именование](./basics/naming.md) — Как называть переменные, файлы и прочие сущности в коде. +- [Архитектура: Обзор](./basics/architecture/index.md) — Архитектурный подход проекта: что такое SLM и как он устроен. +- [Архитектура: Слои](./basics/architecture/reference/layers.md) — Из каких слоёв состоит SLM-архитектура и как они связаны. +- [Архитектура: Модули](./basics/architecture/reference/modules.md) — Что такое модуль в SLM-архитектуре и как он устроен. +- [Архитектура: Сегменты](./basics/architecture/reference/segments.md) — Что такое сегмент модуля в SLM-архитектуре и какие они бывают. +- [Стиль кода](./basics/code-style.md) — Как оформляется код в проекте. +- [Документирование](./basics/documentation.md) — Что и как документировать в коде. +- [Типизация](./basics/typing.md) — Как типизируется код в проекте. + +## Создание проекта + +- [Из шаблона](./creating-project/from-template.md) — Создание нового проекта на основе готового шаблона. +- [По гайду вручную](./creating-project/manual.md) — Поэтапное создание нового проекта без использования шаблона. +- [Чистый Next.js](./creating-project/nextjs.md) — Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку. + +## Работа с данными + +- [Введение](./data/index.md) — Какие источники данных используются в проекте и как с ними работать. +- [REST](./data/rest/index.md) — Как правильно работать с REST API в проекте. +- [REST: Создание клиента](./data/rest/clients/index.md) — Как выбрать способ создания REST-клиента и где размещать его части. +- [REST: Создание клиента: Автогенерация из OpenAPI](./data/rest/clients/auto.md) — Генерация REST-клиента из OpenAPI-спецификации через @gromlab/api-codegen. +- [REST: Создание клиента: Ручное создание](./data/rest/clients/manual.md) — Создание REST-клиента вручную, когда OpenAPI нет или он неполный. +- [REST: Создание клиента: GET-хуки REST-клиента](./data/rest/clients/hooks.md) — Прозрачные SWR-обёртки над GET-методами REST-клиента. +- [REST: Использование: Стратегии получения данных](./data/rest/strategies/index.md) — Как выбрать способ получения REST-данных в зависимости от места и сценария. +- [REST: Использование: Серверный await](./data/rest/strategies/server-await.md) — Получение REST-данных на сервере прямым await метода клиента. +- [REST: Использование: Параллельные серверные запросы](./data/rest/strategies/parallel-server-requests.md) — Как запускать независимые REST-запросы на сервере без waterfall. +- [REST: Использование: Передача промиса ниже](./data/rest/strategies/pass-promise-down.md) — Как запускать серверный REST-запрос выше и ожидать его во вложенном server-компоненте. +- [REST: Использование: Начальные данные для клиентских хуков](./data/rest/strategies/client-hooks-initial-data.md) — Как передать серверный промис в SWR fallback, чтобы клиентские GET-хуки получили начальные данные. +- [REST: Использование: Клиентский GET-хук](./data/rest/strategies/client-get-hook.md) — Получение REST-данных в Client Components через готовые GET-хуки REST-клиента. +- [REST: Использование: Business-композиция](./data/rest/strategies/business-composition.md) — Когда REST-данные нужно объединить или интерпретировать в бизнес-модуле. +- [Realtime](./data/realtime.md) — Работа с push-данными от сервера: подписки и события. + +## Прикладные разделы + +- [Структура проекта](./applied/project-structure.md) — Из чего состоит проект и где что лежит. +- [Страницы](./applied/page-level.md) — Как работать со страницами и другими файлами роутинга Next.js App Router. +- [Компонент](./applied/component.md) — Как создавать React-компоненты внутри SLM-модулей. +- [Модуль](./applied/module.md) — Как создавать и организовывать SLM-модули в проекте. +- [Стили: Настройка](./applied/styles/styles-setup.md) — Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили. +- [Стили: Использование](./applied/styles/styles-usage.md) — Как пишутся стили в проекте. +- [SVG-спрайты](./applied/svg-sprites/svg-sprites-intro.md) — Что такое SVG-спрайты и какие проблемы они решают. +- [SVG-спрайты: Настройка](./applied/svg-sprites/svg-sprites-setup.md) — Подключение SVG-спрайтов в новом проекте. +- [SVG-спрайты: Использование](./applied/svg-sprites/svg-sprites-usage.md) — Как добавлять и использовать SVG-иконки в коде. +- [Изображения](./applied/images.md) — Как подключать изображения через Next.js Image в проекте. +- [Шрифты](./applied/fonts.md) — Как подключать шрифты через Next.js Font в проекте. +- [Алиасы импортов](./applied/aliases.md) — Какие алиасы импортов есть в проекте и как ими пользоваться. +- [Шаблоны генерации](./applied/templates/templates-intro.md) — Что такое шаблоны кодогенерации и какие проблемы они решают. +- [Шаблоны генерации: Настройка](./applied/templates/templates-setup.md) — Первичная установка шаблонов кодогенерации в проект. +- [Шаблоны генерации: Создание шаблонов](./applied/templates/templates-create.md) — Структура шаблонов, синтаксис переменных и примеры. +- [Шаблоны генерации: Использование](./applied/templates/templates-usage.md) — Генерация файлов из шаблонов через VS Code плагин и CLI. +- [Biome](./applied/biome.md) — Установка и настройка линтера-форматтера в новом проекте. +- [PostCSS](./applied/postcss.md) — Установка и настройка CSS-процессора в новом проекте. +- [VS Code](./applied/vscode.md) — Единые настройки редактора и расширений для команды. +- [Локализация](./applied/localization.md) — Как организовать локализацию как infrastructure-модуль. diff --git a/ai/nextjs-style-guide/VERSION b/ai/nextjs-style-guide/VERSION new file mode 100644 index 0000000..2ff96d1 --- /dev/null +++ b/ai/nextjs-style-guide/VERSION @@ -0,0 +1,2 @@ +e835210 +2026-04-30T13:02:04.343Z diff --git a/ai/nextjs-style-guide/applied/aliases.md b/ai/nextjs-style-guide/applied/aliases.md new file mode 100644 index 0000000..da6ccab --- /dev/null +++ b/ai/nextjs-style-guide/applied/aliases.md @@ -0,0 +1,77 @@ +--- +title: Алиасы импортов +description: Какие алиасы импортов есть в проекте и как ими пользоваться. +keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app, layouts, screens, widgets, business, infrastructure, ui, shared] +--- + +# Алиасы импортов + +Какие алиасы импортов есть в проекте и как ими пользоваться. + +## Конфиг + +`tsconfig.json` в корне проекта: + +```json +{ + "compilerOptions": { + "paths": { + "app/*": ["./src/app/*"], + "layouts/*": ["./src/layouts/*"], + "screens/*": ["./src/screens/*"], + "widgets/*": ["./src/widgets/*"], + "business/*": ["./src/business/*"], + "infrastructure/*": ["./src/infrastructure/*"], + "ui/*": ["./src/ui/*"], + "shared/*": ["./src/shared/*"] + } + } +} +``` + +Восемь алиасов — ровно по числу слоёв. Других алиасов в проекте нет. + +## Правила + +- **Каждый импорт между модулями — через алиас слоя.** Относительные пути (`../../`) запрещены за пределами своего модуля. +- **Внутри одного модуля** допустимы относительные импорты (`./model`, `./ui/button`) — это часть инкапсуляции модуля. +- **Префикс `@/` не используется.** Имя слоя — само по себе адрес. +- **Направление импортов** определяется архитектурой, не алиасами. Алиас разрешает импорт технически, но не отменяет правила слоёв (→ [Слои](../basics/architecture/reference/layers.md)). + +**Хорошо** + +```ts +import { Button } from 'ui/button' +import { useUser } from 'business/user' +import { formatDate } from 'shared/utils/date' +``` + +**Плохо** + +```ts +// Относительный путь между модулями +import { Button } from '../../../ui/button' + +// Префикс @/, которого нет в paths +import { Button } from '@/ui/button' + +// Алиас на src — не предусмотрен +import { Button } from 'src/ui/button' +``` + +## Внутри модуля + +Внутри своего модуля — относительные пути: + +```ts +// src/ui/button/button.tsx +import styles from './button.module.css' +import { Icon } from './icon' +``` + +Не использовать алиас на самого себя: + +```ts +// Плохо — алиас вместо относительного пути внутри модуля +import { Icon } from 'ui/button/icon' +``` diff --git a/ai/nextjs-style-guide/applied/biome.md b/ai/nextjs-style-guide/applied/biome.md new file mode 100644 index 0000000..b8a8483 --- /dev/null +++ b/ai/nextjs-style-guide/applied/biome.md @@ -0,0 +1,81 @@ +--- +title: Biome +description: Установка и настройка линтера-форматтера в новом проекте. +keywords: [biome, линтер, форматтер, lint, format, biome.json, "@biomejs/biome", замена eslint, замена prettier] +--- + +# Biome + +Установка и настройка линтера-форматтера в новом проекте. + +## Требования + +- Node.js 18+. +- Проект без установленного ESLint и Prettier (они конфликтуют с Biome). + +## Установка + +1. Установить пакет: + + ```bash + npm install --save-dev --save-exact @biomejs/biome + ``` + +2. Инициализировать конфиг: + + ```bash + npx @biomejs/biome init + ``` + + В корне появится `biome.json` с дефолтными настройками. + +3. Привести `biome.json` к стандартному виду — добавить override для `*.css` (см. «Стандартный `biome.json`»). Делается сразу после `init`, до первого запуска `lint`/`check`. + +4. Добавить скрипты в `package.json`: + + ```json + { + "scripts": { + "lint": "biome lint .", + "format": "biome format --write .", + "check": "biome check --write ." + } + } + ``` + + | Скрипт | Что делает | + |--------|-----------| + | `lint` | Проверка правил без правок | + | `format` | Автоформатирование всех файлов | + | `check` | Lint + format + organize imports в один проход (основная команда) | + +## Стандартный `biome.json` + +Дефолтный `biome.json`, созданный `biome init`, кастомизируется ровно одним блоком — `overrides` для `*.css` с отключённым правилом `suspicious/noUnknownAtRules`. Этот override **обязателен по умолчанию во всех проектах**, независимо от того, подключены ли уже стили: проектный CSS-стек использует `@custom-media` и другие нестандартные at-правила, которые Biome не распознаёт; без override `npm run lint` падает. + +Фрагмент, который добавляется в `biome.json`: + +```jsonc +{ + "overrides": [ + { + "includes": ["**/*.css"], + "linter": { + "rules": { + "suspicious": { + "noUnknownAtRules": "off" + } + } + } + } + ] +} +``` + +Если в `biome.json` уже есть массив `overrides` — добавить элемент в него; не дублировать массив. + +Прочая настройка правил Biome — отдельная задача, не входит в стандартный канон. + +## Интеграция с VS Code + +Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [VS Code](./vscode.md). diff --git a/ai/nextjs-style-guide/applied/component.md b/ai/nextjs-style-guide/applied/component.md new file mode 100644 index 0000000..4e825c3 --- /dev/null +++ b/ai/nextjs-style-guide/applied/component.md @@ -0,0 +1,201 @@ +--- +title: Компонент +description: Как создавать React-компоненты внутри SLM-модулей. +--- + +# Компонент + +Как создавать React-компоненты внутри SLM-модулей. + +## Назначение + +Компонент — минимальная UI-единица проекта. Это один `.tsx` файл без собственной папки, сегментов и публичного API. + +Компонент может использовать стили, типы, хуки и другие ресурсы родительского модуля. Наличие связанных файлов в `styles/` или `types/` не превращает компонент в модуль. + +## Компонент или модуль + +Классификация определяется границей владения: + +- `component` — один `.tsx` файл внутри модуля; +- `module` — папка с `index.ts`, сегментами и собственной публичной границей. + +```text +user/ +├── ui/ +│ └── user-avatar.tsx # компонент +├── styles/ +│ └── user-avatar.module.css # ресурс родительского модуля +├── types/ +│ └── user-avatar.type.ts # ресурс родительского модуля +└── user.tsx # корневой компонент модуля +``` + +`user-avatar.tsx` остаётся компонентом, потому что у него нет собственной папки, `index.ts` и сегментов. + +## Где размещать + +Компонент размещается внутри модуля: + +- В корне модуля, если это главная UI-сущность модуля. +- В `ui/`, если это вспомогательный компонент модуля. + +```text +user/ +├── ui/ +│ └── user-avatar.tsx +├── styles/ +│ ├── user.module.css +│ └── user-avatar.module.css +├── types/ +│ ├── user.type.ts +│ └── user-avatar.type.ts +├── user.tsx +└── index.ts +``` + +`user.tsx` — корневой компонент модуля. `ui/user-avatar.tsx` — вспомогательный компонент этого же модуля. + +## Что запрещено + +- Заворачивать компонент в папку: `ui/header/header.tsx`. +- Создавать для компонента отдельный `index.ts`. +- Создавать собственные сегменты внутри папки компонента: `ui/header/styles/`, `ui/header/types/`, `ui/header/hooks/` и т.п. +- Декларировать внутри `.tsx` сторы, сервисы, API-клиенты, мапперы или утилиты. Для этого есть сегменты родительского модуля. +- Размещать бизнес-правила прямо в компоненте. Компонент может использовать готовые зависимости модуля, но не определяет их. +- Размещать компонент в `parts/` напрямую. `parts/` содержит только модули. + +**Плохо** + +```text +user/ +└── ui/ + └── user-avatar/ + ├── styles/ + │ └── user-avatar.module.css + ├── user-avatar.tsx + └── index.ts +``` + +**Хорошо** + +```text +user/ +├── ui/ +│ └── user-avatar.tsx +├── styles/ +│ └── user-avatar.module.css +└── types/ + └── user-avatar.type.ts +``` + +## Стили и типы + +Компонент использует ресурсы родительского модуля. + +`styles/` и `types/` рядом с корневым компонентом — это сегменты модуля, а не собственные папки `.tsx` файла. + +- CSS Module компонента лежит в `styles/` родительского модуля и называется по компоненту: `user-avatar.module.css`. +- Если у компонента есть CSS Module, его корневой класс всегда называется `.root`. +- Типы компонента лежат в `types/` родительского модуля и называются по компоненту: `user-avatar.type.ts`. +- Локальный `type Props` внутри `.tsx` не используется. Типы пропсов всегда выносятся в `types/` родительского модуля. +- Экспорт типа из `types/` не делает его публичным API. Публичным он становится только при реэкспорте из `index.ts` модуля. + +Причина `.root`: в DevTools проще находить корневой DOM-узел компонента и отличать его от внутренних элементов. + +## Реализация + +- Компоненты объявляются через `const`. +- `React.FC` не используется. +- JSDoc-комментарий обязателен для компонента. +- Пропсы деструктурируются в теле компонента. +- `className` объединяется с `styles.root` через `cl()`. +- Побочные эффекты и состояние выносятся в хуки модуля, если перестают быть тривиальными. +- Компонент возвращает JSX и не содержит orchestration-код страницы или бизнес-домена. + +`user/types/user-avatar.type.ts` + +```ts +import type { ImageProps } from 'next/image' + +/** + * Параметры UserAvatar. + */ +export type UserAvatarParams = {} + +/** Пропсы базового изображения. */ +type RootAttrs = ImageProps + +export type UserAvatarProps = RootAttrs & UserAvatarParams +``` + +`user/ui/user-avatar.tsx` + +```tsx +import cl from 'clsx' +import Image from 'next/image' +import type { UserAvatarProps } from '../types/user-avatar.type' +import styles from '../styles/user-avatar.module.css' + +/** + * Аватар пользователя. + * + * Используется для: + * - отображения пользователя в карточке + * - отображения пользователя в шапке профиля + */ +export const UserAvatar = (props: UserAvatarProps) => { + const { className, ...imageProps } = props + + return +} +``` + +`user/styles/user-avatar.module.css` + +```css +.root { + display: block; + border-radius: 50%; +} +``` + +## Когда нужен модуль + +Решение о выделении модуля остаётся за разработчиком. Поднимать компонент в модуль стоит, если он становится самостоятельной областью: + +- получает свои вложенные компоненты; +- получает свои хуки, стор или сервисы; +- получает внутренние мапперы или утилиты; +- требует собственного публичного API; +- начинает переиспользоваться вне родительского модуля; +- становится отдельной зоной параллельной разработки. + +Пример: страница — это screen-модуль, а самостоятельные секции страницы — вложенные модули в `parts/`. + +```text +screens/home/ +├── parts/ +│ ├── hero-section/ +│ │ ├── styles/ +│ │ │ └── hero-section.module.css +│ │ ├── types/ +│ │ │ └── hero-section.type.ts +│ │ ├── hero-section.tsx +│ │ └── index.ts +│ └── features-section/ +│ ├── styles/ +│ │ └── features-section.module.css +│ ├── types/ +│ │ └── features-section.type.ts +│ ├── features-section.tsx +│ └── index.ts +├── styles/ +│ └── home.module.css +├── types/ +│ └── home.type.ts +├── home.screen.tsx +└── index.ts +``` + +`hero-section` и `features-section` — модули, потому что это самостоятельные части страницы со своей структурой и публичной точкой входа. diff --git a/ai/nextjs-style-guide/applied/fonts.md b/ai/nextjs-style-guide/applied/fonts.md new file mode 100644 index 0000000..981276b --- /dev/null +++ b/ai/nextjs-style-guide/applied/fonts.md @@ -0,0 +1,128 @@ +--- +title: Шрифты +description: Как подключать шрифты через Next.js Font в проекте. +--- + +# Шрифты + +Как подключать шрифты через Next.js Font в проекте. + +## Назначение + +Шрифты подключаются через `next/font`. Это стандартный способ Next.js: шрифты загружаются без ручных ``, `@font-face` и настройки preconnect. + +Шрифт подключается в точке инициализации приложения, а в CSS используется через переменную. + +## Google Fonts + +```tsx +// src/app/layout.tsx +import type { ReactNode } from 'react' +import { Inter } from 'next/font/google' +import 'shared/styles/global.css' + +const inter = Inter({ + subsets: ['latin', 'cyrillic'], + variable: '--font-main', + display: 'swap', +}) + +type RootLayoutProps = { + children: ReactNode +} + +export default function RootLayout({ children }: RootLayoutProps) { + return ( + + {children} + + ) +} +``` + +```css +/* src/shared/styles/global.css */ +body { + font-family: var(--font-main), system-ui, sans-serif; +} +``` + +## Локальные шрифты + +Каждый локальный шрифт размещается в отдельной папке внутри `src/shared/fonts/`. В этой же папке лежит `.font.ts`, где объявляется `localFont`. + +```text +src/shared/fonts/ +└── roboto/ + ├── roboto.font.ts + ├── Roboto-Regular.woff2 + ├── Roboto-Italic.woff2 + ├── Roboto-Bold.woff2 + └── Roboto-BoldItalic.woff2 +``` + +```ts +// src/shared/fonts/roboto/roboto.font.ts +import localFont from 'next/font/local' + +export const roboto = localFont({ + src: [ + { + path: './Roboto-Regular.woff2', + weight: '400', + style: 'normal', + }, + { + path: './Roboto-Italic.woff2', + weight: '400', + style: 'italic', + }, + { + path: './Roboto-Bold.woff2', + weight: '700', + style: 'normal', + }, + { + path: './Roboto-BoldItalic.woff2', + weight: '700', + style: 'italic', + }, + ], + variable: '--font-main', + display: 'swap', +}) +``` + +`app/` импортирует готовый объект шрифта и только подключает его к документу: + +```tsx +// src/app/layout.tsx +import type { ReactNode } from 'react' +import { roboto } from 'shared/fonts/roboto/roboto.font' +import 'shared/styles/global.css' + +type RootLayoutProps = { + children: ReactNode +} + +export default function RootLayout({ children }: RootLayoutProps) { + return ( + + {children} + + ) +} +``` + +Путь в `localFont` указывается относительно `.font.ts`, поэтому файлы шрифта импортируются коротко: `./Roboto-Regular.woff2`. + +Если шрифтов несколько, у каждого своя папка и свой `.font.ts`. + +## Правила + +- Использовать `next/font/google` или `next/font/local`. +- Не подключать шрифты через ручные `` и `@font-face` без необходимости. +- Подключать шрифты один раз — в корневом layout через готовый объект шрифта. +- Использовать CSS-переменные `variable`, а не жёстко прописывать семейство в каждом компоненте. +- Локальные файлы шрифтов хранить в `src/shared/fonts/{font-name}/` рядом с `{font-name}.font.ts`. +- Не объявлять `localFont` внутри `src/app/layout.tsx`; layout только импортирует готовый шрифт. diff --git a/ai/nextjs-style-guide/applied/images.md b/ai/nextjs-style-guide/applied/images.md new file mode 100644 index 0000000..448efd8 --- /dev/null +++ b/ai/nextjs-style-guide/applied/images.md @@ -0,0 +1,95 @@ +--- +title: Изображения +description: Как подключать изображения через Next.js Image в проекте. +--- + +# Изображения + +Как подключать изображения через Next.js Image в проекте. + +## Назначение + +Изображения рендерятся через компонент `Image` из `next/image`. Это сохраняет единый API для размеров, `alt`, lazy-loading и `priority`, даже если оптимизация изображений отключена. + +В проекте оптимизация Next.js Image отключается через `unoptimized`, чтобы сборка и рантайм не зависели от встроенного image optimizer. + +## Настройка + +Отключение оптимизации задаётся глобально в `next.config.ts`: + +```ts +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + images: { + unoptimized: true, + }, +} + +export default nextConfig +``` + +После этого `unoptimized` не нужно повторять на каждом `Image`. + +## Использование + +Статические изображения, доступные по URL, размещаются в `public/`: + +```text +public/ +└── images/ + └── user-avatar.png +``` + +```tsx +import Image from 'next/image' + +export const UserAvatar = () => { + return ( + Аватар пользователя + ) +} +``` + +## Правила + +- Использовать `Image` из `next/image`, не обычный ``. +- Для контентных изображений всегда писать осмысленный `alt`. +- Для декоративных изображений использовать `alt=""`. +- Указывать `width` и `height`, если изображение не использует `fill`. +- При `fill` задавать `sizes` и контролировать размеры родителя стилями. +- `priority` ставить только для изображений первого экрана. +- SVG-иконки не оформлять как изображения — для них используется раздел [SVG-спрайты](./svg-sprites/svg-sprites-intro.md). + +## Пример с `fill` + +```tsx +import Image from 'next/image' +import styles from '../styles/article-card-cover.module.css' + +export const ArticleCardCover = () => { + return ( +
+ Обложка статьи +
+ ) +} +``` + +```css +.root { + position: relative; + aspect-ratio: 16 / 9; + overflow: hidden; +} +``` diff --git a/ai/nextjs-style-guide/applied/localization.md b/ai/nextjs-style-guide/applied/localization.md new file mode 100644 index 0000000..c65b7f6 --- /dev/null +++ b/ai/nextjs-style-guide/applied/localization.md @@ -0,0 +1,81 @@ +--- +title: Локализация +description: Как организовать локализацию как infrastructure-модуль. +--- + +# Локализация + +Как организовать локализацию как infrastructure-модуль. + +## Назначение + +Локализация — инфраструктурная подсистема приложения. Она отвечает за текущую локаль, словари, форматирование переводов и API для компонентов. + +Код локализации живёт в `src/infrastructure/i18n/`. Компоненты и модули не читают словари напрямую — они используют публичный API infrastructure-модуля. + +## Структура + +```text +src/infrastructure/i18n/ +├── config/ +│ └── i18n.config.ts +├── dictionaries/ +│ ├── ru.ts +│ └── en.ts +├── hooks/ +│ └── use-translation.hook.ts +├── providers/ +│ └── i18n-provider.tsx +├── types/ +│ └── i18n.type.ts +└── index.ts +``` + +Набор сегментов может отличаться, но публичная точка входа остаётся одна — `infrastructure/i18n`. + +## Подключение + +`app/` только подключает готовый провайдер локализации. Реализация провайдера, словари и конфиг остаются в `infrastructure/i18n/`. + +```tsx +// src/app/layout.tsx +import type { ReactNode } from 'react' +import { I18nProvider } from 'infrastructure/i18n' + +type RootLayoutProps = { + children: ReactNode +} + +export default function RootLayout({ children }: RootLayoutProps) { + return ( + + + {children} + + + ) +} +``` + +## Использование + +Компоненты получают переводы через готовый API модуля локализации: + +```tsx +import { useTranslation } from 'infrastructure/i18n' + +export const ProfileTitle = () => { + const { t } = useTranslation() + + return

{t('profile.title')}

+} +``` + +## Правила + +- Локализация живёт в `infrastructure/i18n/`. +- `app/` только подключает готовый provider и передаёт locale. +- Словари не импортируются напрямую в компоненты, screens или business-модули. +- Ключи переводов не собираются динамически из строк, если это ломает типизацию и поиск. +- Тексты интерфейса не хардкодятся в переиспользуемых компонентах, если они должны переводиться. +- Форматирование дат, чисел и валют должно проходить через API локализации или отдельные утилиты infrastructure-модуля. diff --git a/ai/nextjs-style-guide/applied/module.md b/ai/nextjs-style-guide/applied/module.md new file mode 100644 index 0000000..5e6ca5c --- /dev/null +++ b/ai/nextjs-style-guide/applied/module.md @@ -0,0 +1,162 @@ +--- +title: Модуль +description: Как создавать и организовывать SLM-модули в проекте. +--- + +# Модуль + +Как создавать и организовывать SLM-модули в проекте. + +## Назначение + +Модуль — основной строительный блок SLM-архитектуры. Это папка с публичным API (`index.ts`) и опциональными сегментами: компонентами, стилями, типами, хуками, сторами, сервисами и вложенными модулями. + +Если UI-сущность остаётся одним `.tsx` файлом и использует ресурсы родительского модуля — это [компонент](./component.md), а не модуль. Связанные файлы в `styles/` и `types/` родителя не создают новую модульную границу. + +Архитектурное определение: [Модули SLM](../basics/architecture/reference/modules.md). Список сегментов: [Сегменты SLM](../basics/architecture/reference/segments.md). + +## Когда нужен модуль + +Создавайте модуль, если сущности нужны: + +- публичный API; +- хуки, сторы, сервисы, мапперы или утилиты; +- вложенные части; +- переиспользование вне родительского модуля; +- самостоятельная ответственность на слое. + +Если понадобилась папка вокруг компонента — это сигнал, что нужен модуль. + +## Где размещать + +Модуль размещается на самом низком уровне использования. + +- Нужен только одному модулю — размещается в `parts/` родителя. +- Нужен одной странице — размещается в `screens/{name}/parts/`. +- Нужен одному layout — размещается в `layouts/{name}/parts/`. +- Переиспользуется между страницами или layout — поднимается в `widgets/`. +- Представляет бизнес-домен — размещается в `business/`. +- Является UI-китом — размещается в `ui/`. + +`app/` не содержит модулей. Это слой файлового роутинга и инициализации. + +## Структура + +Минимальный UI-модуль: + +```text +user/ +├── user.tsx +└── index.ts +``` + +Модуль расширяется сегментами только при реальной потребности: + +```text +user/ +├── ui/ +│ └── user-avatar.tsx +├── parts/ +│ └── user-posts/ +│ ├── user-posts.tsx +│ └── index.ts +├── styles/ +│ ├── user.module.css +│ └── user-avatar.module.css +├── types/ +│ ├── user.type.ts +│ └── user-avatar.type.ts +├── user.tsx +└── index.ts +``` + +Корневой компонент опционален. Business- и infrastructure-модули могут состоять только из хуков, сервисов, типов и публичного API. + +## `ui/` и `parts/` + +`ui/` содержит только компоненты: отдельные `.tsx` файлы без собственных сегментов. + +`parts/` содержит только модули: каждая запись внутри `parts/` — папка с собственным `index.ts`. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не кладутся. + +```text +user/ +├── ui/ +│ └── user-avatar.tsx # компонент +└── parts/ + └── user-posts/ # модуль + ├── styles/ + │ └── user-posts.module.css + ├── user-posts.tsx + └── index.ts +``` + +Если компоненту в `ui/` понадобились стили или типы, они добавляются в `styles/` и `types/` родительского модуля. Если компоненту нужны собственные хуки, вложенные части или публичная граница — он переносится в `parts/` как модуль. + +## Публичный API + +`index.ts` — единственная точка входа в модуль. Внешние импорты внутренних файлов запрещены. + +```ts +// user/index.ts +export { User } from './user' +export type { UserProps } from './types/user.type' +``` + +```ts +// Плохо: импорт в обход публичного API. +import { UserPosts } from 'screens/user/parts/user-posts/user-posts' + +// Хорошо: импорт через публичный API родительского модуля. +import { User } from 'screens/user' +``` + +Вложенный модуль имеет свой `index.ts`, но наружу родителя экспортируется только при необходимости. + +## Именование + +Базовые правила описаны в разделе [Именование](../basics/naming.md). + +- Папка модуля — `kebab-case`: `user-posts/`. +- Файл корневого компонента повторяет имя папки: `user-posts/user-posts.tsx`. +- Корневые модули слоёв наследуют роль слоя в имени файла: `screens/profile/profile.screen.tsx`, `layouts/main/main.layout.tsx`. +- Корневой компонент именуется в `PascalCase`: `UserPosts`. +- Если имя без контекста слишком общее, добавляется префикс родителя или роль слоя: `ProfileUserPosts`, `ProfileScreen`, `MainLayout`. + +## Примеры + +### Screen-модуль + +```text +screens/profile/ +├── ui/ +│ └── profile-heading.tsx +├── parts/ +│ └── activity-feed/ +│ ├── styles/ +│ │ └── activity-feed.module.css +│ ├── activity-feed.tsx +│ └── index.ts +├── styles/ +│ ├── profile.module.css +│ └── profile-heading.module.css +├── types/ +│ ├── profile.type.ts +│ └── profile-heading.type.ts +├── profile.screen.tsx +└── index.ts +``` + +### Business-модуль без корневого компонента + +```text +business/auth/ +├── hooks/ +│ └── use-auth.hook.ts +├── services/ +│ └── auth.service.ts +├── stores/ +│ └── auth.store.ts +├── types/ +│ └── auth.type.ts +└── index.ts +``` diff --git a/ai/nextjs-style-guide/applied/page-level.md b/ai/nextjs-style-guide/applied/page-level.md new file mode 100644 index 0000000..27ffea9 --- /dev/null +++ b/ai/nextjs-style-guide/applied/page-level.md @@ -0,0 +1,186 @@ +--- +title: Файлы роутинга +description: Как работать со страницами и другими файлами роутинга Next.js App Router. +--- + +# Файлы роутинга + +Как работать со страницами и другими файлами роутинга Next.js App Router. + +## Назначение + +`src/app/**` — точка входа приложения и слой файлового роутинга Next.js. + +Файлы роутинга не реализуют интерфейс. Они описывают маршрут: читают параметры, получают данные первого рендера, подготавливают кеш или состояние и передают результат в screen. + +Границы слоя описаны в [Архитектура → Слои → App](../basics/architecture/reference/layers.md#слой-app). + +## Граница ответственности + +| Область | Где живёт | +|---|---| +| Файлы маршрутов (`page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`) | `src/app/**` | +| Параметры маршрута, `metadata`, `redirect()`, `notFound()` | `src/app/**` | +| Серверные запросы для первого рендера | `src/app/**`, через готовые клиенты и сервисы нижних слоёв | +| Прогрев SWR-кеша, начальное состояние, подключение провайдеров | `src/app/**`, только через готовые обёртки из нижних слоёв | +| UI страницы | `screens/` | +| Каркас страницы: header, footer, sidebar | `layouts/` | +| Провайдеры, сторы, хуки, API-клиенты, сервисы | нижние слои (`screens/`, `business/`, `infrastructure/`, `shared/`) | +| CSS Modules и стили компонентов | рядом с компонентами, не в `src/app/**` | + +## Что можно делать в `page.tsx` + +- Экспортировать `metadata` или `generateMetadata`. +- Читать `params` и `searchParams`. +- Нормализовать и валидировать параметры маршрута. +- Делать серверные запросы для первого рендера через готовые клиенты или сервисы. +- Вызывать `redirect()` и `notFound()`. +- Готовить начальные данные для screen. +- Готовить SWR `fallback` и передавать его в готовый провайдер. +- Подключать готовый провайдер стора страницы и передавать начальное состояние. +- Рендерить screen или композицию из готовых обёрток и screen. + +## Что запрещено + +- Писать UI-разметку страницы прямо в файле роутинга. +- Создавать локальные компоненты внутри `src/app/**`. +- Добавлять CSS Modules, стили компонентов, `components/`, `styles/`, `hooks/`, `stores/`, `services/` внутри `src/app/**`. +- Реализовывать провайдеры, сторы, хуки, API-клиенты или сервисы в файлах роутинга. +- Размещать бизнес-логику, мапперы и правила предметной области в файлах роутинга. +- Вызывать `useSWR` и доменные клиентские хуки в файлах роутинга. + +## Страницы + +Страница объявляется через `export default function`. Для серверных запросов используется `async function`. + +```tsx +import type { Metadata } from 'next' +import { ProfileScreen } from 'screens/profile' + +export const metadata: Metadata = { + title: 'Профиль', + description: 'Страница профиля пользователя', +} + +type ProfilePageProps = { + params: Promise<{ id: string }> +} + +export default async function ProfilePage({ params }: ProfilePageProps) { + const { id } = await params + + return +} +``` + +## Данные первого рендера + +Если данные нужны до первого рендера, `page.tsx` получает их на сервере и передаёт в screen. Сам запрос выполняется через готовый клиент или сервис нижнего слоя. + +```tsx +import { notFound } from 'next/navigation' +import { userApi } from 'infrastructure/backend-api' +import { UserScreen } from 'screens/user' + +type UserPageProps = { + params: Promise<{ id: string }> +} + +export default async function UserPage({ params }: UserPageProps) { + const { id } = await params + const user = await userApi.users.get(id) + + if (!user) { + notFound() + } + + return +} +``` + +Если данные нужны нескольким клиентским SWR-хукам, файл роутинга может обернуть дерево в `SWRConfig` и передать `fallback`. Запросы стартуют на сервере, а клиентские хуки получают данные из кеша. + +Ключи `fallback` должны совпадать с ключами внутри GET-хуков REST-клиента. Для array-key используется `unstable_serialize`. + +```tsx +import type { ReactNode } from 'react' +import { SWRConfig, unstable_serialize } from 'swr' +import { + backendApi, + getCurrentUserKey, + getPostListKey, +} from 'infrastructure/backend-api' + +type FeedLayoutProps = { + children: ReactNode +} + +export default async function FeedLayout({ children }: FeedLayoutProps) { + const userPromise = backendApi.user.getCurrent() + const postsPromise = backendApi.posts.list() + + return ( + + {children} + + ) +} +``` + +Подробнее о стратегиях запросов и начальных данных для клиентских хуков: [REST → Стратегии получения данных](../data/rest/strategies/index.md), [REST → Начальные данные для клиентских хуков](../data/rest/strategies/client-hooks-initial-data.md). + +## Инициализация состояния + +Файл роутинга может подключить готовый провайдер стора страницы, если состояние зависит от маршрута или данных первого рендера. Реализация стора и провайдера не размещается в `src/app/**`. + +```tsx +import { ProfileScreen, ProfileStoreProvider } from 'screens/profile' + +type ProfilePageProps = { + params: Promise<{ id: string }> +} + +export default async function ProfilePage({ params }: ProfilePageProps) { + const { id } = await params + + return ( + + + + ) +} +``` + +## Layout + +`layout.tsx` подключает готовую инициализацию приложения: глобальные стили, провайдеры и верхнеуровневые обёртки из нижних слоёв. + +Вёрстка layout-каркаса выносится в слой `layouts/`. Реализация провайдеров, стилей и UI не размещается в `app/`. + +## Error и Not Found + +`error.tsx` и `not-found.tsx` делегируют разметку готовым screen или widget. В файле роутинга остаётся только адаптация API Next.js к пропсам нижнего слоя. + +```tsx +'use client' + +import { ErrorScreen } from 'screens/error' + +type ErrorPageProps = { + error: Error & { digest?: string } + reset: () => void +} + +const ErrorPage = ({ error, reset }: ErrorPageProps) => { + return +} + +export default ErrorPage +``` diff --git a/ai/nextjs-style-guide/applied/postcss.md b/ai/nextjs-style-guide/applied/postcss.md new file mode 100644 index 0000000..38c6283 --- /dev/null +++ b/ai/nextjs-style-guide/applied/postcss.md @@ -0,0 +1,70 @@ +--- +title: PostCSS +description: Установка и настройка CSS-процессора в новом проекте. +keywords: [postcss, postcss.config.mjs, postcss-custom-media, postcss-nesting, autoprefixer, postcss-global-data, csstools, "@custom-media", "@nest", css-процессор] +--- + +# PostCSS + +Установка и настройка CSS-процессора в новом проекте. + +## Зачем PostCSS + +Подключаем ради двух вещей: + +- **Вложенность** — `&:hover`, `&::before`, `&._active` и `@media` внутри селектора. Без процессора нативный CSS не покрывает всех нужных кейсов вложенности. +- **`@custom-media`** — единые breakpoints проекта (`@media (--md)`) вместо магических `min-width`. Определяются в одном месте, переиспользуются везде. + +Autoprefixer и `@csstools/postcss-global-data` идут довеском под эти две задачи. + +## Требования + +- Next.js 14+ (App Router). +- Node.js 18+. + +CSS Modules поддерживаются Next.js из коробки — отдельной установки не требуют. + +## Установка + +1. Установить PostCSS-плагины как devDependencies: + + ```bash + npm install -D postcss-custom-media postcss-nesting autoprefixer @csstools/postcss-global-data + ``` + +2. Создать `postcss.config.mjs` в корне проекта (см. «Конфиг»). + +## Конфиг + +Файл `postcss.config.mjs` в корне проекта. + +```js +// postcss.config.mjs +export default { + plugins: { + '@csstools/postcss-global-data': { + files: ['src/shared/styles/media.css'], + }, + 'postcss-custom-media': {}, + 'postcss-nesting': {}, + autoprefixer: {}, + }, +} +``` + +### Разбор плагинов + +| Плагин | Назначение | +|--------|------------| +| `@csstools/postcss-global-data` | Подгружает определения `@custom-media` из `src/shared/styles/media.css` перед обработкой каждого CSS-модуля. Семантика — «глобальный файл определений, который не импортируется в исходники» | +| `postcss-custom-media` | Поддержка `@custom-media --md (...)` и использования `@media (--md) {}`. Определения берутся из файла, который подгрузил `postcss-global-data` | +| `postcss-nesting` | Нативная CSS-вложенность: `&:hover`, `&::before`, `&._active` | +| `autoprefixer` | Добавление вендорных префиксов по browserslist | + +### Почему внешний файл с `@custom-media`, а не `@import` + +`@custom-media` — глобальные определения, одинаковые для всего проекта. Держим их в `src/shared/styles/media.css`. `@csstools/postcss-global-data` подгружает этот файл перед каждым модулем, а `postcss-custom-media` заменяет `@media (--md)` на конкретные `@media (min-width: ...)` на этапе сборки. Сами определения в бандл не попадают. + +Опция `importFrom` у `postcss-custom-media` удалена в v10+; её роль теперь выполняет `@csstools/postcss-global-data`. + +Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование стилей](./styles/styles-usage.md), раздел «Импорт стилей»). diff --git a/ai/nextjs-style-guide/applied/project-structure.md b/ai/nextjs-style-guide/applied/project-structure.md new file mode 100644 index 0000000..02d50f4 --- /dev/null +++ b/ai/nextjs-style-guide/applied/project-structure.md @@ -0,0 +1,101 @@ +--- +title: Структура проекта +description: Из чего состоит проект и где что лежит. +--- + +# Структура проекта + +Из чего состоит проект и где что лежит. + +## Корень репозитория + +```text +project-root/ +├── .templates/ # Шаблоны для генерации модулей +├── .vscode/ # Настройки и рекомендуемые расширения VS Code +├── public/ # Статика, доступная по прямому URL +├── src/ # Исходный код приложения +├── .env.example # Переменные окружения проекта (шаблон) +├── .env # Переменные окружения проекта (не коммитить) +├── .gitignore +├── AGENTS.md # Инструкции для AI-агентов +├── biome.json # Линтер и форматтер (вместо ESLint + Prettier) +├── next.config.ts # Конфигурация Next.js +├── package.json # Зависимости и скрипты +├── postcss.config.mjs # Конфигурация PostCSS +└── tsconfig.json # Конфигурация TypeScript +``` + +## Папка `public/` + +Хранит статические файлы, которые отдаются по прямому URL без обработки сборщиком: + +```text +public/ +└── og-image.png +``` + +Компоненты, стили и другой исходный код здесь не размещаются. + +## Папка `src/` + +```text +src/ +├── app/ # Роутинг Next.js и точка входа приложения +├── layouts/ # Каркасы страниц (header, footer, sidebar) +├── screens/ # Контент конкретной страницы +├── widgets/ # Составные блоки интерфейса, не привязанные к домену +├── business/ # Бизнес-домены (auth, catalog, orders) +├── infrastructure/ # Техсервисы (theme, i18n, API-адаптеры) +├── ui/ # UI-кит без бизнес-логики +└── shared/ # Общие ресурсы (утилиты, типы, стили) +``` + +Принципы организации слоёв описаны в разделе [Архитектура](../basics/architecture/). + +### Папка `app/` + +Точка входа приложения и файловый роутинг Next.js (`layout.tsx`, `page.tsx`, route-сегменты). +`app/` подключает готовую инициализацию из нижних слоёв, но не реализует провайдеры, стили, UI-компоненты, хуки, сторы или сервисы. + +Подробнее о границах слоя: [Архитектура → Слои → App](../basics/architecture/reference/layers.md#слой-app). + +```text +src/app/ +├── layout.tsx # Корневой layout +└── page.tsx # Главная страница +``` + +## Папка `.templates/` + +Содержит шаблоны для генерации кода. Каждый подкаталог — шаблон отдельного типа модуля: + +```text +.templates/ +├── component/ # Шаблон компонента +├── screen/ # Шаблон экрана +├── layout/ # Шаблон layout +├── widget/ # Шаблон виджета +├── module/ # Шаблон бизнес-модуля +└── store/ # Шаблон стора +``` + +Подробнее о генерации описано в разделе [Шаблоны генерации](./templates/templates-intro.md). + +## Конфигурационные файлы + +| Файл | Назначение | +|---|---| +| `next.config.ts` | Настройки Next.js: редиректы, переменные окружения, webpack | +| `tsconfig.json` | Настройки TypeScript: пути, строгость, таргет | +| `biome.json` | Правила линтера и форматтера Biome | +| `postcss.config.mjs` | Подключение PostCSS-плагинов (CSS Modules, custom media) | +| `package.json` | Зависимости, версии, npm-скрипты | +| `AGENTS.md` | Инструкции для AI-агентов, работающих в проекте | + +## Переменные окружения + +- `.env` — переменные окружения проекта, запрещено коммитить +- `.env.example` — шаблон, коммитится в репозиторий + +Переменные с префиксом `NEXT_PUBLIC_` доступны в клиентском коде. Остальные доступны только на сервере. diff --git a/src/infrastructure/.gitkeep b/ai/nextjs-style-guide/applied/stores.md similarity index 100% rename from src/infrastructure/.gitkeep rename to ai/nextjs-style-guide/applied/stores.md diff --git a/ai/nextjs-style-guide/applied/styles/styles-setup.md b/ai/nextjs-style-guide/applied/styles/styles-setup.md new file mode 100644 index 0000000..64ea0e9 --- /dev/null +++ b/ai/nextjs-style-guide/applied/styles/styles-setup.md @@ -0,0 +1,176 @@ +--- +title: Настройка стилей +description: "Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили." +keywords: [variables.css, media.css, global.css, shared/styles, токены, переменные, custom-media, breakpoints, подключение стилей, базовые стили, инициализация] +--- + +# Настройка стилей + +Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили. + +## Требования + +- Установлен PostCSS или любой другой pre/post-процессор с поддержкой `@custom-media`. + +## Файлы + +Состав глобальных стилей — три файла: + +| Файл | Роль | +|------|------| +| `variables.css` | Токены проекта (цвета, отступы, радиусы) | +| `media.css` | Custom media queries (брейкпоинты по ширине и высоте) | +| `global.css` | Точка сборки глобальных стилей: через `@import` тянет все остальные глобалы, импортируется в приложение один раз | + +Правила подключения: + +- В приложение импортируется **только** `global.css`. +- `variables.css` и будущие глобальные файлы (резеты, темы, типографика) подключаются в `global.css` через `@import`. +- `media.css` **не импортируется** — ни в `global.css`, ни в компоненты, ни в точку инициализации. Его читает CSS-процессор на этапе сборки (см. [PostCSS](../postcss.md)). + +## Корневой `font-size` + +Базовая единица `rem` в проекте привязана к **16px**: корневой `font-size` не переопределяется. `html { font-size: ... }` писать запрещено — пользовательская настройка размера шрифта в браузере должна работать (a11y). Все `rem`-значения в `media.css` и других стилях трактуются как `1rem = 16px по умолчанию`. + +Reset браузерных дефолтов (`box-sizing`, сброс `margin`, типографика) каноном не задаётся — каждый проект решает сам. Если заводится — подключается через `global.css`. + +## Установка + +### 1. Создать файлы + +```bash +mkdir -p src/shared/styles +touch src/shared/styles/variables.css src/shared/styles/media.css src/shared/styles/global.css +``` + +### 2. Заполнить `media.css` + +Файл `src/shared/styles/media.css`. Стандартный набор брейкпоинтов проекта; редактировать только при согласованном изменении шкалы. + +Единица — `rem` (реагирует на корневой `font-size`). Перевод исходит из дефолтного `html { font-size: 16px }`, т.е. `1rem = 16px`. + +```css +/* src/shared/styles/media.css */ + +/* Ширина — Mobile First (min-width), кроме --xs (max-width) */ +@custom-media --xs (max-width: 35.9375rem); /* 575px — до sm */ +@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 — full HD+ */ + +/* Высота — min-height */ +@custom-media --h-xs (min-height: 41.6875rem); /* 667px — iPhone SE портрет */ +@custom-media --h-sm (min-height: 43.875rem); /* 702px */ +@custom-media --h-md (min-height: 50.625rem); /* 810px — iPad портрет */ +@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 */ +``` + +Правила: + +- только `@custom-media` на верхнем уровне; +- имена короткие, по шкале (`--xs` … `--3xl`); высотные — с префиксом `--h-`; +- единица — `rem`, не `em`/`px`; пиксельное значение указывается комментарием; +- значения ширины — `min-width` (Mobile First), исключение `--xs` — `max-width` (блок «строго меньше `--sm`»); +- значения высоты — `min-height`. + +### 3. Заполнить `variables.css` + +Файл `src/shared/styles/variables.css`. Набор токенов под проект расширяется по мере роста дизайн-системы. + +```css +/* src/shared/styles/variables.css */ +:root { + /* Цвета */ + --color-primary: #3b82f6; + --color-bg: #ffffff; + --color-bg-hover: #f5f5f5; + --color-text: #1a1a1a; + + /* Отступы */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + + /* Скругления */ + --radius-1: 4px; + --radius-2: 8px; +} +``` + +Правила: + +- все токены определяются в `:root` — без вложенных селекторов; +- именование — `kebab-case` по ролям: `--color-*`, `--space-*`, `--radius-*`; +- `px` — основная единица для пространственных токенов; +- темы накладываются поверх через `[data-theme="..."] { ... }` — в отдельном файле темы или здесь же. + +`variables.css` напрямую в приложение не импортируется — только через `global.css`. + +### 4. Заполнить `global.css` + +Файл `src/shared/styles/global.css`. Единственный глобальный файл, импортируемый в точку инициализации приложения. Внутри — `@import` остальных глобалов относительным путём. + +```css +/* src/shared/styles/global.css */ +@import './variables.css'; + +/* Сюда же подключаются будущие глобалы через @import: + * @import './reset.css'; + * @import './typography.css'; + * @import './themes.css'; + * media.css НЕ импортируется — он работает через PostCSS. + */ +``` + +Правила: + +- пути в `@import` — относительные (`./variables.css`), не через алиасы; нативный CSS `@import` не понимает tsconfig-paths; +- `media.css` в `global.css` **не импортируется**; +- собственные глобальные правила (`html { ... }`, `body { ... }`) писать **не здесь**, а в отдельных файлах рядом (`reset.css`, `typography.css`) и подключать через `@import`. `global.css` — только точка сборки; +- порядок `@import` определяет порядок каскада: токены первыми, дальше резеты / темы / типографика. + +### 5. Подключить `global.css` в layout + +Импорт делается **один раз** — в корневом layout приложения: + +```tsx +// src/app/layout.tsx +import 'shared/styles/global.css' + +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'App', + description: '', +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} +``` + +`variables.css` и `media.css` в layout **не импортируются напрямую** — только через `global.css` (variables) или через PostCSS на сборке (media). + +## Проверка установки + +- В `src/shared/styles/` присутствуют три файла: `variables.css`, `media.css`, `global.css`. В `src/app/` папки `styles/` нет. +- В `src/app/layout.tsx` есть `import 'shared/styles/global.css'`. Импортов `variables.css` и `media.css` там нет. +- В проекте **не появились** PostCSS-пакеты и `postcss.config.*` — этот раздел их не ставит. +- `npm run build` завершается успешно. + +## Дальше + +- [PostCSS](../postcss.md) — подключить процессор, чтобы заработали `@media (--md)` и вложенность. +- [Использование стилей](./styles-usage.md) — правила написания CSS в компонентах. +- [SVG-спрайты](../svg-sprites/svg-sprites-setup.md) — стили иконок отдельно от глобальных. diff --git a/ai/nextjs-style-guide/applied/styles/styles-usage.md b/ai/nextjs-style-guide/applied/styles/styles-usage.md new file mode 100644 index 0000000..d7f1405 --- /dev/null +++ b/ai/nextjs-style-guide/applied/styles/styles-usage.md @@ -0,0 +1,271 @@ +--- +title: Использование стилей +description: Как пишутся стили в проекте. +--- + +# Использование стилей + +Как пишутся стили в проекте. + +## Общие правила + +- Только **PostCSS** и **CSS Modules** для кастомной стилизации. +- Подход **Mobile First** — стили пишутся от мобильных к десктопу. +- Именование классов — `camelCase` (`.root`, `.buttonNext`, `.itemTitle`). +- Корневой класс каждого CSS Module компонента всегда называется `.root` — это упрощает ориентацию в DevTools и отладку DOM. +- Модификаторы — отдельный класс с `_`, применяется через `&._modifier`. + +**Хорошо** +```css +.submitButton { + padding: 8px 16px; + + &._disabled { + opacity: 0.5; + } +} +``` + +**Плохо** +```css +/* Плохо: kebab-case и вложенный элемент вместо отдельного класса. */ +.submit-button { + padding: 8px 16px; + + &__icon { + margin-right: 8px; + } +} +``` + +## Вложенность + +- Вложенность селекторов запрещена. +- Исключения: + - Псевдоклассы: `&:hover`, `&:active`, `&:focus`, `&:disabled` и т.д. + - Псевдоэлементы: `&::before`, `&::after`. + - Медиа-запросы: `@media`. + - Модификаторы: `&._active`, `&._disabled`. +- Каждый вложенный блок отделяется пустой строкой от предыдущих свойств. + +**Хорошо** +```css +.card { + padding: 16px; + background-color: var(--color-bg); + + &:hover { + background-color: var(--color-bg-hover); + } + + &::after { + content: ''; + display: block; + } + + &._highlighted { + border-color: var(--color-primary); + } + + @media (--md) { + padding: 24px; + } +} + +.cardTitle { + font-size: 16px; + + @media (--md) { + font-size: 20px; + } +} +``` + +**Плохо** +```css +/* Плохо: вложенность селекторов, нет пустых строк между блоками. */ +.card { + padding: 16px; + .cardTitle { + font-size: 16px; + } + &:hover { + background-color: var(--color-bg-hover); + } +} +``` + +## Медиа-запросы + +- Только **Custom Media Queries**: `@media (--md) {}`. +- Запрещены произвольные breakpoints: `@media (min-width: 768px)`. +- `@media` пишется только **внутри** селектора. +- Запрещено писать `@media` на верхнем уровне с селекторами внутри. + +**Хорошо** +```css +.sidebar { + display: none; + + @media (--md) { + display: block; + } +} + +.sidebarTitle { + font-size: 14px; + + @media (--md) { + font-size: 18px; + } +} +``` + +**Плохо** +```css +/* Плохо: @media на верхнем уровне с селекторами внутри. */ +@media (--md) { + .sidebar { + display: block; + } + + .sidebarTitle { + font-size: 18px; + } +} + +/* Плохо: произвольный breakpoint вместо custom media. */ +.sidebar { + @media (min-width: 992px) { + display: block; + } +} +``` + +## CSS-переменные + +- Цвета (`--color-*`), отступы (`--space-*`), скругления (`--radius-*`) определяются в `src/shared/styles/variables.css` через `:root`. +- Файл переменных подключается через `src/shared/styles/global.css`, который импортируется один раз в `src/app/layout.tsx`. +- Не дублировать магические значения в компонентах. + +**Хорошо** +```css +/* src/shared/styles/variables.css */ +:root { + --color-primary: #3b82f6; + --color-bg: #ffffff; + --color-bg-hover: #f5f5f5; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --radius-1: 4px; + --radius-2: 8px; +} +``` + +```css +/* компонент */ +.card { + padding: var(--space-3); + border-radius: var(--radius-2); + background-color: var(--color-bg); +} +``` + +**Плохо** +```css +/* Плохо: магические значения вместо переменных. */ +.card { + padding: 12px; + border-radius: 8px; + background-color: #ffffff; +} +``` + +## Custom Media + +- Breakpoints определяются через Custom Media Queries в `src/shared/styles/media.css`. +- Custom media подключаются глобально через конфиг PostCSS (плагин `postcss-custom-media`) — не импортировать в файлы стилей. + +```css +/* src/shared/styles/media.css */ +@custom-media --sm (min-width: 36em); +@custom-media --md (min-width: 62em); +@custom-media --lg (min-width: 82em); +``` + +## Импорт стилей + +- Стили компонента импортируются только внутри своего компонента. +- Запрещено импортировать стили одного компонента в другой. +- Custom media не импортируются в файлы стилей — они подключаются глобально через конфиг PostCSS. + +## Форматирование + +- Пустая строка между селекторами верхнего уровня. +- Пустая строка перед каждым вложенным блоком (медиа, псевдокласс, модификатор). + +**Хорошо** +```css +.userBar { + display: none; + color: var(--color-text); + + @media (--md) { + display: flex; + } +} + +.userBarButton { + background-color: var(--color-bg); + + &:hover { + background-color: var(--color-bg-hover); + } + + &._active { + background-color: var(--color-primary); + } +} +``` + +**Плохо** +```css +/* Плохо: нет пустых строк между селекторами и вложенными блоками. */ +.userBar { + display: none; + color: var(--color-text); + @media (--md) { + display: flex; + } +} +.userBarButton { + background-color: var(--color-bg); + &:hover { + background-color: var(--color-bg-hover); + } + &._active { + background-color: var(--color-primary); + } +} +``` + +## Единицы измерения + +- `px` — основная единица измерения. +- Остальные (`em`, `rem`, `%`, `vh`/`vw`) — допускаются по необходимости дизайна. + +## Порядок CSS-свойств + +В стилях рекомендуется придерживаться логического порядка свойств: + +1. Позиционирование (`position`, `top`, `left`, `z-index`). +2. Блочная модель (`display`, `width`, `height`, `margin`, `padding`). +3. Оформление (`background`, `border`, `box-shadow`, `border-radius`). +4. Текст (`font`, `color`, `text-align`, `line-height`). +5. Прочее (`transition`, `animation`, `opacity`, `cursor`). + +## Комментарии + +- Желательно не писать комментарии в CSS. +- Исключение — нетривиальные хаки и обходные решения, к которым стоит оставить пояснение. diff --git a/ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-intro.md b/ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-intro.md new file mode 100644 index 0000000..4bc1fb2 --- /dev/null +++ b/ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-intro.md @@ -0,0 +1,31 @@ +--- +title: SVG-спрайты +description: "Что такое SVG-спрайты и какие проблемы они решают." +--- + +# SVG-спрайты + +Что такое SVG-спрайты и какие проблемы они решают. + +## Проблема + +Иконки в проекте — это десятки и сотни SVG-файлов, которые нужно как-то доставлять в интерфейс. Подход «один `` на иконку» или инлайн SVG в каждом компоненте приводят к трём проблемам: + +- **Дублирование.** Инлайн SVG в нескольких компонентах — один и тот же код размазан по проекту. Изменение иконки требует правок в десяти местах. +- **Размер бандла.** Каждый инлайн SVG — полный XML-код, который попадает в JS-бандл. Сотня иконок × средний размер SVG = сотни килобайт, которые браузер парсит как JavaScript, а не как статику. +- **Нет управления цветом.** Инлайн SVG жёстко закрашивает иконку. Сменить цвет по состоянию (`:hover`, `._disabled`) — значит дублировать SVG или городить `currentColor`-хаки в каждом компоненте. + +## Решение + +SVG-спрайты — это единый файл-контейнер, в который собираются все иконки проекта. В коде используется один React-компонент ``, а браузер загружает спрайт как статику — один раз, с кешированием. + +Что дают SVG-спрайты: + +- **Один источник.** Каждая иконка — один SVG-файл в `src/shared/sprites/`. Обновил файл — иконка обновилась везде. +- **Лёгкий бандл.** Спрайт отдаётся как статический файл из `public/`, не попадает в JavaScript. Типы имён иконок генерируются автоматически — автодополнение работает без ручных описаний. +- **Цвет через CSS.** При сборке цвета в SVG заменяются на CSS-переменные. Цвет иконки меняется через `color` родителя или через переменные `--icon-color-N` — как любой другой стиль. + +## Состав раздела + +- [Настройка](./svg-sprites-setup.md) — подключение пакета, конфигурация, первая генерация. +- [Использование](./svg-sprites-usage.md) — добавление иконок, компонент ``, управление цветом. diff --git a/ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-setup.md b/ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-setup.md new file mode 100644 index 0000000..7329443 --- /dev/null +++ b/ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-setup.md @@ -0,0 +1,132 @@ +--- +title: Настройка SVG-спрайтов +description: Подключение SVG-спрайтов в новом проекте. +keywords: [svg-sprites, установка, настройка, config, пакет, "@gromlab/svg-sprites", svg-sprites.config.ts] +--- + +# Настройка SVG-спрайтов +Подключение SVG-спрайтов в новом проекте. + +## Установка + +1. Установить пакет: + + ```bash + npm install @gromlab/svg-sprites + ``` + +2. Создать `svg-sprites.config.ts` в корне проекта (см. [Стандартный конфиг](#стандартныи-конфиг)). + +3. Создать папку входа для SVG-файлов в слое `shared`: + + ```bash + mkdir -p src/shared/sprites/icons + ``` + + Источники спрайтов живут в `src/shared/sprites//` — это слой `shared` SLM-архитектуры (см. [Структура проекта](../project-structure.md), [Архитектура](../../basics/architecture/index.md)). В `src/` посторонних каталогов вне слоёв не заводим. + +4. Добавить скрипты в `package.json`: + + ```json + { + "scripts": { + "sprite": "svg-sprites", + "predev": "svg-sprites", + "prebuild": "svg-sprites" + } + } + ``` + + Хуки `predev` и `prebuild` гарантируют, что спрайты и типы всегда актуальны перед запуском и сборкой. + +5. Добавить сгенерированные артефакты в `.gitignore`: + + ```text + # Сгенерированные спрайты и React-компонент + /public/sprites/ + /src/ui/svg-sprite/ + ``` + + 6. Выполнить первую генерацию: + + ```bash + npm run sprite + ``` + +7. Подключить спрайт в layout. Глобальный спрайт (иконки) подключается через `` в корневом layout — браузер загрузит файл заранее и закеширует: + + ```tsx + // src/app/layout.tsx + import 'shared/styles/global.css' + + import type { Metadata } from 'next' + + export const metadata: Metadata = { + title: 'App', + description: '', + } + + export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + {children} + + ) + } + ``` + + Локальные спрайты (если есть) подключаются аналогично в layout конкретной страницы или маршрута. + +## Стандартный конфиг + +Файл `svg-sprites.config.ts` в корне проекта. Это канон — отклонения только по явной причине. + +```ts +// svg-sprites.config.ts +import { defineConfig } from '@gromlab/svg-sprites' + +export default defineConfig({ + output: 'public/sprites', + publicPath: '/sprites', + react: 'src/ui/svg-sprite', + sprites: [ + { name: 'icons', input: 'src/shared/sprites/icons' }, + ], +}) +``` + +### Фиксированные значения + +| Опция | Значение | Почему так | +|-------|----------|------------| +| `output` | `public/sprites` | Единая папка статики Next.js | +| `publicPath` | `/sprites` | URL-путь без `public/` (Next.js раздаёт `public/` как `/`) | +| `react` | `src/ui/svg-sprite` | Слой `ui/` из архитектуры проекта (→ [Архитектура](../../basics/architecture/index.md)) | +| `sprites[0].name` | `icons` | Основной спрайт всегда называется `icons` | + +### Трансформации + +Все значения по умолчанию оставлять включёнными: + +```ts +transform: { + removeSize: true, + replaceColors: true, + addTransition: true, +} +``` + +Явно прописывать блок `transform` не нужно — пакет применяет эти значения по умолчанию. + +Отключать `replaceColors` — только для отдельного спрайта с фиксированной палитрой (например, брендовые логотипы). Делать это на уровне спрайта, не глобально. + +### Режим + +По умолчанию `mode: 'stack'` — не указывать явно. Переход на `symbol` требует обоснования: превью и примеры в пакете оптимизированы под `stack`. + +## Дальше + +- [Использование](./svg-sprites-usage.md) — добавление иконок, компонент ``, управление цветом. diff --git a/ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-usage.md b/ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-usage.md new file mode 100644 index 0000000..d984642 --- /dev/null +++ b/ai/nextjs-style-guide/applied/svg-sprites/svg-sprites-usage.md @@ -0,0 +1,56 @@ +--- +title: Использование SVG-спрайтов +description: Как добавлять и использовать SVG-иконки в коде. +keywords: [svg, спрайт, sprite, иконка, icon, SvgSprite, превью, preview, цвет, color] +--- + +# Использование SVG-спрайтов + +Как добавлять и использовать SVG-иконки в коде. + +## Шаги + +1. **Положить SVG в папку спрайта:** + + ```text + src/shared/sprites/icons/new-icon.svg + ``` + +2. **Импортировать компонент.** Компонент `` генерируется пакетом вместе с типами имён иконок — автодополнение работает без ручных описаний: + + ```tsx + import { SvgSprite } from 'ui/svg-sprite' + + + ``` + +3. **Посмотреть и пощупать иконку — в превью.** Пакет генерирует HTML-превью рядом со спрайтом (`public/sprites/icons.preview.html`). Там виден набор иконок, имена и поведение цвета. + +## Управление цветом + +При сборке цвета в SVG заменяются на CSS-переменные `--icon-color-N`. Управление — через обычный CSS родителя. + +**Моно-иконка** наследует `color` родителя (`--icon-color-1` по умолчанию `currentColor`): + +```css +.button { + color: var(--color-primary); +} +``` + +**Точечное переопределение** — через переменную: + +```css +.icon-danger { + --icon-color-1: var(--color-danger); +} +``` + +**Мульти-иконка** — переменные задаются явно, порядок виден в превью: + +```css +.folder { + --icon-color-1: var(--color-folder-bg); + --icon-color-2: var(--color-folder-accent); +} +``` diff --git a/ai/nextjs-style-guide/applied/templates/templates-create.md b/ai/nextjs-style-guide/applied/templates/templates-create.md new file mode 100644 index 0000000..1715c5d --- /dev/null +++ b/ai/nextjs-style-guide/applied/templates/templates-create.md @@ -0,0 +1,91 @@ +--- +title: Создание шаблонов генерации +description: "Структура шаблонов, синтаксис переменных и примеры." +keywords: [шаблоны, templates, .templates, syntax, переменные, kebabCase, pascalCase, scaffold] +--- + + +::: v-pre + +# Создание шаблонов генерации + +Структура шаблонов, синтаксис переменных и примеры. + +## Структура шаблонов + +Все шаблоны лежат в `.templates/` в корне проекта. Каждая папка — отдельный шаблон. + +```text +.templates/ +├── component/ # шаблон компонента +│ └── {{name.kebabCase}}/ +│ ├── styles/ +│ │ └── {{name.kebabCase}}.module.css +│ ├── types/ +│ │ └── {{name.kebabCase}}.type.ts +│ ├── {{name.kebabCase}}.tsx +│ └── index.ts +└── store/ # шаблон Zustand стора + └── {{name.kebabCase}}/ + ├── {{name.kebabCase}}.store.ts + ├── {{name.kebabCase}}.type.ts + └── index.ts +``` + +## Синтаксис шаблонов + +### Переменные + +Переменные работают в именах файлов/папок и внутри файлов: + +```text +{{variable}} +``` + +Переменные могут быть любыми. `name` — дефолтная, подставляется генератором автоматически. Если реализация требует дополнительных параметров — можно использовать произвольные наборы переменных. + +### Модификаторы + +Модификаторы меняют регистр и формат записи переменной: + +```text +{{name.pascalCase}} → MyButton +{{name.camelCase}} → myButton +{{name.kebabCase}} → my-button +{{name.snakeCase}} → my_button +{{name.screamingSnakeCase}} → MY_BUTTON +``` + +## Как создать новый шаблон + +1. Создать папку в `.templates/` с именем шаблона (например `hook`). +2. Внутри разместить файлы и папки, используя `{{name}}` и модификаторы в именах и содержимом. +3. Шаблон сразу доступен и в расширении VS Code, и в CLI. + +Пример — создание шаблона для хука: + +```text +.templates/ +└── hook/ + └── {{name.kebabCase}}/ + ├── {{name.kebabCase}}.hook.ts + └── index.ts +``` + +```ts +// .templates/hook/{{name.kebabCase}}.hook.ts +export const {{name.camelCase}} = () => { + +} +``` + +```ts +// .templates/hook/index.ts +export { {{name.camelCase}} } from './{{name.kebabCase}}.hook' +``` + +## Дальше + +- [Использование](./templates-usage.md) — генерация через VS Code плагин и CLI. + +::: diff --git a/ai/nextjs-style-guide/applied/templates/templates-intro.md b/ai/nextjs-style-guide/applied/templates/templates-intro.md new file mode 100644 index 0000000..1ae1b95 --- /dev/null +++ b/ai/nextjs-style-guide/applied/templates/templates-intro.md @@ -0,0 +1,32 @@ +--- +title: Шаблоны генерации +description: "Что такое шаблоны кодогенерации и какие проблемы они решают." +--- + +# Шаблоны генерации + +Что такое шаблоны кодогенерации и какие проблемы они решают. + +## Проблема + +Каждый новый модуль в проекте — компонент, стор, бизнес-модуль — требует однотипной структуры файлов и boilerplate-кода. Ручное создание приводит к трём проблемам: + +- **Расхождения.** Разные разработчики создают модули по-разному: забывают `index.ts`, называют типы не по канону, пропускают стили. +- **Время.** Создание одного компонента с типами, стилями и экспортом — 5–10 минут рутины. За спринт набегают часы. +- **Ошибки копипасты.** Копирование существующего модуля и переименование — источник опечаток и забытых ссылок. + +## Решение + +Шаблоны кодогенерации — это папки с файлами-заготовками в `.templates/`. Вместо ручного создания файлов разработчик вызывает генератор, указывает имя — и получает готовый модуль со всей структурой, именами и boilerplate, подставленными автоматически. + +Что дают шаблоны: + +- **Единообразие.** Все модули одного типа идентичны по структуре. Канон живёт в шаблоне, а не в памяти разработчика. +- **Скорость.** Генерация модуля — одна команда. Остальное время — на бизнес-логику. +- **Согласованность с архитектурой.** Шаблоны учитывают SLM: правильные слои, сегменты, экспорты. Отклонение от стайлгайда требует осознанного усилия, а не случайного упущения. + +## Состав раздела + +- [Настройка](./templates-setup.md) — первичная установка: скачивание стандартного набора шаблонов в проект. +- [Создание шаблонов](./templates-create.md) — структура файлов, синтаксис переменных, примеры. +- [Использование](./templates-usage.md) — генерация через VS Code плагин и CLI. diff --git a/ai/nextjs-style-guide/applied/templates/templates-setup.md b/ai/nextjs-style-guide/applied/templates/templates-setup.md new file mode 100644 index 0000000..4365c71 --- /dev/null +++ b/ai/nextjs-style-guide/applied/templates/templates-setup.md @@ -0,0 +1,44 @@ +--- +title: Настройка шаблонов генерации +description: Первичная установка шаблонов кодогенерации в проект. +keywords: [шаблоны, templates, .templates, tiged, generator, генератор шаблонов, скачать шаблоны, scaffold] +--- + +# Настройка шаблонов генерации + +Первичная установка шаблонов кодогенерации в проект. + +## Установка + +1. Проверить, что `.templates/` отсутствует (или согласовать перезапись, если папка уже есть). + +2. Скачать папку из эталонного репозитория: + + ```bash + npx tiged git@gromlab.ru:templates/nextjs-template.git/.templates .templates + ``` + +3. Если `tiged` падает в default-режиме (HTTP-tarball у Gitea) — повторить с явным git-режимом: + + ```bash + npx tiged --mode=git git@gromlab.ru:templates/nextjs-template.git/.templates .templates + ``` + +4. Проверить генерацию: + + ```bash + npx @gromlab/create component test src/ui + ``` + + После проверки — удалить тестовый модуль. + +## Проверка установки + +- В корне проекта есть папка `.templates/`. +- Внутри `.templates/` присутствуют стандартные шаблоны (или согласованный кастомный набор). +- Пробная генерация через `npx @gromlab/create ...` отрабатывает без ошибок. + +## Дальше + +- [Создание шаблонов](./templates-create.md) — структура файлов, синтаксис переменных, примеры. +- [Использование](./templates-usage.md) — генерация через VS Code плагин и CLI. diff --git a/ai/nextjs-style-guide/applied/templates/templates-usage.md b/ai/nextjs-style-guide/applied/templates/templates-usage.md new file mode 100644 index 0000000..1d2e2d8 --- /dev/null +++ b/ai/nextjs-style-guide/applied/templates/templates-usage.md @@ -0,0 +1,39 @@ +--- +title: Использование шаблонов генерации +description: Генерация файлов из шаблонов через VS Code плагин и CLI. +keywords: [шаблоны, templates, generate, VS Code, CLI, gromlab/create, npx, scaffold] +--- + +# Использование шаблонов генерации + +Генерация файлов из шаблонов через VS Code плагин и CLI. + +## Через VS Code + +Template File Generator | gromlab ([Marketplace](https://marketplace.visualstudio.com/items?itemName=gromlab.vscode-templateFileGenerator), [Open VSX](https://open-vsx.org/extension/gromlab/vscode-templateFileGenerator)) — расширение для генерации файлов и папок из шаблонов через интерфейс редактора. + +1. ПКМ на целевой папке в проводнике VS Code. +2. **Generate from template** → выбрать шаблон. +3. Ввести имя (например `button`) — расширение подставит его во все переменные `{{name}}`. + +Расширение устанавливается разово на машину разработчика, не через проект. + +## Через CLI + +[@gromlab/create](https://www.npmjs.com/package/@gromlab/create) — CLI для генерации из тех же шаблонов. Используется через npx, глобальная установка не требуется. + +```bash +npx @gromlab/create <шаблон> <имя> [путь] +``` + +Путь не обязателен — по умолчанию генерация происходит в текущую директорию. + +| Команда | Что создаёт | +|---|---| +| `npx @gromlab/create component button` | Компонент в текущей папке | +| `npx @gromlab/create module auth src/business` | Бизнес-модуль | +| `npx @gromlab/create widget header src/widgets` | Виджет | +| `npx @gromlab/create layout admin src/layouts` | Layout | +| `npx @gromlab/create store auth src/business/auth/stores` | Стор | + +CLI вызывается через `npx`, в `package.json` отдельно не добавляется. diff --git a/ai/nextjs-style-guide/applied/vscode.md b/ai/nextjs-style-guide/applied/vscode.md new file mode 100644 index 0000000..f844ead --- /dev/null +++ b/ai/nextjs-style-guide/applied/vscode.md @@ -0,0 +1,88 @@ +--- +title: VS Code +description: Единые настройки редактора и расширений для команды. +--- + +# VS Code + +Единые настройки редактора и расширений для команды. + +## Структура `.vscode/` + +```text +.vscode/ +├── extensions.json # Рекомендуемые расширения +└── settings.json # Настройки редактора для проекта +``` + +Оба файла коммитятся в репозиторий. + +## Расширения + +Файл `.vscode/extensions.json` определяет список расширений, которые VS Code предложит установить при открытии проекта. + +```json +// .vscode/extensions.json +{ + "recommendations": [ + "biomejs.biome", + "MyTemplateGenerator.mytemplategenerator", + "csstools.postcss" + ] +} +``` + +| Расширение | Назначение | +|---|---| +| [Biome](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) | Линтинг и форматирование кода. Заменяет ESLint и Prettier | +| Template File Generator \| gromlab ([Marketplace](https://marketplace.visualstudio.com/items?itemName=gromlab.vscode-templateFileGenerator), [Open VSX](https://open-vsx.org/extension/gromlab/vscode-templateFileGenerator)) | Генерация файлов и папок из шаблонов `.templates/` через контекстное меню | +| [PostCSS Language Support](https://marketplace.visualstudio.com/items?itemName=csstools.postcss) | Подсветка синтаксиса и автодополнение для PostCSS (`@custom-media`, `@nest` и др.) | + +### Зачем это нужно + +- Новый участник команды получает все нужные расширения одним кликом. +- Нет разночтений: все используют одинаковый форматтер и линтер. +- Расширения привязаны к проекту, а не к конкретному разработчику. + +## Настройки редактора + +Файл `.vscode/settings.json` переопределяет пользовательские настройки VS Code на уровне проекта. + +```json +// .vscode/settings.json +{ + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "files.associations": { + "*.css": "postcss" + } +} +``` + +### Разбор настроек + +| Настройка | Значение | Что делает | +|---|---|---| +| `editor.defaultFormatter` | `biomejs.biome` | Biome используется как единственный форматтер для всех файлов | +| `editor.formatOnSave` | `true` | Код автоматически форматируется при каждом сохранении | +| `codeActionsOnSave.source.fixAll.biome` | `explicit` | Biome автоматически применяет безопасные исправления при сохранении | +| `codeActionsOnSave.source.organizeImports.biome` | `explicit` | Импорты сортируются и группируются автоматически при сохранении | +| `files.associations` | `"*.css": "postcss"` | Все CSS-файлы открываются с подсветкой PostCSS вместо стандартного CSS | + +### Зачем это нужно + +- **Единый стиль кода** -- форматирование происходит автоматически, невозможно закоммитить неформатированный код. +- **Автофикс при сохранении** -- распространённые ошибки линтинга исправляются без ручного вмешательства. +- **Сортировка импортов** -- импорты всегда в одном порядке, без конфликтов при мерже. +- **PostCSS-подсветка** -- кастомные at-правила (`@custom-media`, `@define-mixin`) подсвечиваются корректно, а не как ошибки. + +## Что не должно быть в `.vscode/` + +Не коммитятся файлы, специфичные для конкретного разработчика: + +- **Не коммитить**: отладочные конфигурации с локальными путями, персональные сниппеты, настройки тем оформления. +- **Коммитить**: только `extensions.json` и `settings.json` с общими для команды настройками. diff --git a/ai/nextjs-style-guide/basics/architecture/index.md b/ai/nextjs-style-guide/basics/architecture/index.md new file mode 100644 index 0000000..4653ce2 --- /dev/null +++ b/ai/nextjs-style-guide/basics/architecture/index.md @@ -0,0 +1,100 @@ +--- +title: SLM Design +description: "Архитектурный подход проекта: что такое SLM и как он устроен." +--- + +# SLM Design + +Архитектурный подход проекта: что такое SLM и как он устроен. + +## Преимущества + +### Вертикальная организация домена + +Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы. + +### Dependency Injection без фреймворков + +Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий. + +### Разделение ответственности без перегрузки слоёв + +Сервисы приложения (`infrastructure/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода. + +### Горизонтальная инкапсуляция + +Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга. + +### Колокация по умолчанию + +Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями. + +### Явное разделение каркаса и контента + +Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью. + +### Масштабирование через группировку + +При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции). + +## Происхождение + +SLM Design вырос на основе: + +- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей +- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое +- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию +- **Colocation Principle** — код живёт рядом с местом использования + +## Пример структуры проекта + +```text +src/ +├── app/ +│ +├── layouts/ +│ ├── main/ +│ └── dashboard/ +│ +├── screens/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── about/ +│ +├── widgets/ +│ ├── page-heading/ +│ ├── hero-section/ +│ └── promo-banner/ +│ +├── business/ +│ ├── auth/ +│ ├── catalog/ +│ ├── orders/ +│ └── chat/ +│ +├── infrastructure/ +│ ├── theme/ +│ ├── i18n/ +│ ├── backend-api/ +│ └── logger/ +│ +├── ui/ +│ ├── button/ +│ ├── input/ +│ ├── modal/ +│ ├── toast/ +│ └── dropdown/ +│ +└── shared/ + ├── lib/ + ├── types/ + └── styles/ +``` + +## Принципы + +- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле. +- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости. +- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API. +- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда. diff --git a/ai/nextjs-style-guide/basics/architecture/reference/layers.md b/ai/nextjs-style-guide/basics/architecture/reference/layers.md new file mode 100644 index 0000000..c9f3407 --- /dev/null +++ b/ai/nextjs-style-guide/basics/architecture/reference/layers.md @@ -0,0 +1,253 @@ +--- +title: Слои SLM +description: Из каких слоёв состоит SLM-архитектура и как они связаны. +--- + +# Слои SLM + +Из каких слоёв состоит SLM-архитектура и как они связаны. + +## Определение + +**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.** + +## Группы слоёв + +Слои делятся на три группы: + +| Группа | Слои | Описание | +|--------|------|----------| +| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы | +| Ядро | `business`, `infrastructure`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит | +| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги | + +## Направление зависимостей + +Любой импорт между модулями — только через публичный API. + +``` +app → [ layouts | screens ] → widgets → business → infrastructure → ui → shared +``` + +- `layouts` и `screens` — параллельные слои, не импортируют друг друга +- Модули одного слоя в группе «Композиция» изолированы друг от друга +- Модули одного слоя `infrastructure` и `ui` могут импортировать друг друга через публичный API +- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую +- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях + + +## Слой App + +Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen. + +В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации. + +### Требования + +- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация +- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов +- Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует +- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы +- Никем не импортируется + +## Слой Layouts + +Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar). + +```text +src/layouts/ +├── main/ +├── dashboard/ +└── auth/ +``` + +### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Контекстно-зависимые блоки принимает через пропсы от `app`, не импортирует напрямую + +## Слой Screens + +Контент конкретной страницы: собирает её из модулей нижних слоёв. + +```text +src/screens/ +├── home/ +├── products/ +├── product-detail/ +├── about/ +└── contacts/ +``` + +Когда количество страниц затрудняет навигацию — вводится группировка по разделам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/screens/ +├── shop/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── cart/ +├── account/ +│ ├── profile/ +│ ├── settings/ +│ └── order-history/ +└── info/ + ├── about/ + ├── contacts/ + └── faq/ +``` + +### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Локальные одноразовые секции живут внутри screen-модуля, не выносятся в `widgets`/`business` + +## Слой Widgets + +Составной блок интерфейса, который компонует модули ядра, но не принадлежит конкретному бизнес-домену. Widget появляется когда блок используется в нескольких screens или layouts. + +Если блок принадлежит домену — он живёт в `business/{area}/`, даже если переиспользуется. Если блок нужен только в одном месте — это `screens/{name}/parts/` или `layouts/{name}/parts/`, а не widget. + +```text +src/widgets/ +├── page-heading/ +├── hero-section/ +├── onboarding-checklist/ +├── promo-banner/ +└── error-boundary/ +``` + +### Требования + +- Не принадлежит конкретному бизнес-домену. Если блок доменный — он живёт в `business/` +- Используется в нескольких screens или layouts + +## Слой Business + +Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами. + +Слой входит в группу «Ядро». Импортирует `infrastructure/`, `ui/`, `shared/`. Cross-domain зависимости по коду реализуются через фабрику. `import type` между доменами разрешён напрямую. + +Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки. + +```text +src/business/ +├── auth/ +├── catalog/ +├── orders/ +├── checkout/ +└── chat/ +``` + +Когда количество доменов затрудняет навигацию — вводится группировка по субдоменам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/business/ +├── commerce/ +│ ├── catalog/ +│ ├── cart/ +│ ├── orders/ +│ └── checkout/ +└── communication/ + ├── chat/ + └── notifications/ +``` + +### Требования + +- Один модуль = один бизнес-домен +- Циклические зависимости между доменами запрещены +- Импорт кода между доменами — через фабрику. `import type` — напрямую +- Доменные типы (`User`, `Product`) живут здесь, не в `shared/` + +## Слой Infrastructure + +Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль. + +Слой входит в группу «Ядро». Импортирует `infrastructure/`, `ui/`, `shared/`. + +Отличие от `shared/`: infrastructure — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +```text +src/infrastructure/ +├── theme/ +├── i18n/ +├── backend-api/ +├── maps-api/ +├── logger/ +├── feature-flags/ +└── realtime/ +``` + +### Требования + +- Один модуль = один техсервис +- Импортирует `infrastructure/`, `ui/`, `shared/` + +## Слой UI + +UI-кит без бизнес-логики: button, carousel, toast, modal. + +Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`. + +Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`. + +```text +src/ui/ +├── button/ +├── input/ +├── icon/ +├── carousel/ +├── modal/ +├── toast/ +├── dropdown/ +├── tabs/ +└── tooltip/ +``` + +Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах. + +```text +src/ui/ +├── primitives/ +│ ├── button/ +│ ├── input/ +│ ├── icon/ +│ └── badge/ +└── composites/ + ├── carousel/ + ├── modal/ + ├── dropdown/ + ├── tabs/ + └── tooltip/ +``` + +### Требования + +- Не содержит бизнес-логику +- Импортирует только `ui/` и `shared/` + +## Слой Shared + +Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене. + +Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует. + +Отличие от `infrastructure/`: infrastructure — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь. + +```text +src/shared/ +├── lib/ +├── types/ +├── styles/ +└── sprites/ +``` + +### Требования + +- Не имеет runtime-состояния diff --git a/ai/nextjs-style-guide/basics/architecture/reference/modules.md b/ai/nextjs-style-guide/basics/architecture/reference/modules.md new file mode 100644 index 0000000..c5a424a --- /dev/null +++ b/ai/nextjs-style-guide/basics/architecture/reference/modules.md @@ -0,0 +1,165 @@ +--- +title: Модули SLM +description: Что такое модуль в SLM-архитектуре и как он устроен. +--- + +# Модули SLM + +Что такое модуль в SLM-архитектуре и как он устроен. + +## Определение + +**Модуль — универсальный строительный блок архитектуры. Живёт на слое и содержит всё необходимое для своей работы: компоненты, хуки, сторы, сервисы, типы, стили. Набор содержимого не фиксирован — включаются только нужные части.** + +## Модуль vs компонент + +**Компонент** — один `.tsx` файл. Не имеет своих сегментов, использует сегменты родительского модуля. Живёт в корне или `ui/` сегменте модуля. + +**Модуль** — папка, которая может содержать корневой компонент, сегменты (`hooks/`, `types/`, `styles/`, `ui/`, `parts/` и т.д.) и публичный API (`index.ts`). + +```text +auth/ +├── ui/ +│ ├── auth-guard.tsx +│ └── logout-button.tsx +├── parts/ +│ ├── login-form/ +│ ├── registration-form/ +│ └── restore-form/ +├── hooks/ +├── stores/ +├── types/ +├── auth.tsx # корневой компонент (опционален) +└── index.ts +``` + +## Структура + +Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль может состоять даже из одного `index.ts` с реэкспортом типов. + +```text +{module-name}/ +├── {module-name}.tsx # корневой компонент (опционален) +├── ui/ # компоненты модуля (только .tsx) +├── parts/ # вложенные модули (со своими сегментами) +├── hooks/ # хуки +├── stores/ # сторы состояния +├── services/ # внешние источники данных +├── mappers/ # трансформация данных между форматами +├── types/ # типы +├── styles/ # стили +├── lib/ # утилиты модуля +├── config/ # константы +└── index.ts # публичный API +``` + +Подробное описание каждого сегмента — в разделе [Сегменты](./segments.md). + +## Публичный API + +Модуль экспортирует наружу только то, что нужно другим. Всё остальное — внутреннее. + +```ts +// business/auth/index.ts +export type { User, Session } from './types/user.types' +export { useAuth } from './hooks/use-auth.hook' +export { AuthGuard } from './ui/auth-guard' +``` + +Импорт в обход `index.ts` запрещён: + +```ts +// Плохо +import { validateToken } from '@/business/auth/lib/tokens' + +// Хорошо +import { useAuth } from '@/business/auth' +``` + +## Фабрика + +Если модуль зависит от кода другого бизнес-домена — он экспортирует фабрику. Фабрика декларирует необходимые зависимости и возвращает API модуля. Точка использования (screen, widget, layout) предоставляет зависимости при вызове. + +Модуль без cross-domain зависимостей экспортирует API напрямую. Типы всегда экспортируются напрямую — `import type` не является runtime-зависимостью. + +### Модуль без зависимостей — прямой экспорт: + +```ts +// business/auth/index.ts +export { useAuth } from './hooks/use-auth' +export { useCurrentUser } from './hooks/use-current-user' +export type { User, Session } from './types' +``` + +### Модуль с зависимостями — фабрика: + +```ts +// business/chat/types/deps.ts +import type { User } from '@/business/auth' + +export interface ChatDeps { + useCurrentUser: () => User | null +} +``` + +```ts +// business/chat/index.ts +import type { ChatDeps } from './types/deps' + +export function chatFactory(deps: ChatDeps) { + return { + useMessages: (roomId: string) => { + const user = deps.useCurrentUser() + // ... + }, + useSendMessage: (roomId: string) => { + const user = deps.useCurrentUser() + return (text: string) => { /* ... */ } + }, + useChatRooms: () => { + const user = deps.useCurrentUser() + // ... + }, + ChatBadge: ({ count }: { count: number }) => { /* ... */ }, + } +} + +export type { Message, ChatRoom } from './types' +export type { ChatDeps } from './types/deps' +``` + +### Использование на странице: + +```tsx +// screens/support/support.tsx +import { useCurrentUser } from '@/business/auth' +import { chatFactory } from '@/business/chat' + +const chat = chatFactory({ useCurrentUser }) + +export function SupportScreen() { + const { useMessages, useSendMessage, ChatBadge } = chat + const messages = useMessages('support') + const sendMessage = useSendMessage('support') + + return ( +
+ + {messages.map(m => )} + +
+ ) +} +``` + +## Жизненный цикл + +Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности. + +- Нужен на одной странице → `screens/{name}/parts/` +- Появился в 2+ местах → поднимается по природе: + - абстрактный UI → `ui/` + - блок с данными/логикой → `widgets/` + - представление бизнес-домена → `business/{area}/parts/` + +Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. diff --git a/ai/nextjs-style-guide/basics/architecture/reference/segments.md b/ai/nextjs-style-guide/basics/architecture/reference/segments.md new file mode 100644 index 0000000..45af86b --- /dev/null +++ b/ai/nextjs-style-guide/basics/architecture/reference/segments.md @@ -0,0 +1,159 @@ +--- +title: Сегменты SLM +description: Что такое сегмент модуля в SLM-архитектуре и какие они бывают. +--- + +# Сегменты SLM + +Что такое сегмент модуля в SLM-архитектуре и какие они бывают. + +## Определение + +**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.** + +## Обзор + +| Сегмент | Содержимое | +|---------|------------| +| `ui/` | Вспомогательные компоненты модуля — только `.tsx` файлы | +| `parts/` | Вложенные модули со своими сегментами | +| `hooks/` | React-хуки | +| `stores/` | Сторы состояния | +| `services/` | Работа с внешними источниками данных | +| `mappers/` | Трансформация данных между форматами | +| `types/` | TypeScript-типы и интерфейсы | +| `styles/` | Стили | +| `lib/` | Утилиты и хелперы модуля | +| `config/` | Константы и конфигурация | + +## Сегмент ui/ + +Вспомогательные компоненты модуля. Содержит только `.tsx` файлы — без своих сегментов, стилей, типов, хуков и публичного API. Использует сегменты родительского модуля. + +Корневой компонент модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`. + +```text +user/ +├── ui/ +│ ├── user-avatar.tsx +│ └── user-status.tsx +├── types/ +├── hooks/ +├── user.tsx +└── index.ts +``` + +Если компоненту нужны собственные сегменты — это уже не `ui/`, а `parts/`. + +## Сегмент parts/ + +Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются. + +```text +home/ +├── parts/ +│ ├── hero-section/ +│ │ ├── hero-section.tsx +│ │ ├── styles/ +│ │ ├── parts/ +│ │ │ └── top-banner/ +│ │ │ ├── top-banner.tsx +│ │ │ └── index.ts +│ │ └── index.ts +│ └── features-section/ +│ ├── features-section.tsx +│ ├── hooks/ +│ └── index.ts +├── home.screen.tsx +└── index.ts +``` + +Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — вспомогательный компонент, один `.tsx` файл. + +Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке. + +Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше. + +## Сегмент hooks/ + +React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты. + +```text +hooks/ +├── use-auth.hook.ts +├── use-session.hook.ts +└── use-permissions.hook.ts +``` + +## Сегмент stores/ + +Сторы состояния модуля. Конкретная реализация зависит от выбранного стейт-менеджера (Zustand, MobX, Redux и т.д.). + +```text +stores/ +├── auth.store.ts +└── session.store.ts +``` + +## Сегмент services/ + +Работа с внешними источниками данных: API-вызовы, запросы, подписки. + +```text +services/ +├── auth.service.ts +└── token.service.ts +``` + +## Сегмент mappers/ + +Функции трансформации данных из одного формата в другой: DTO в доменный тип, доменный тип в DTO, доменный тип в ViewModel. + +```text +mappers/ +├── map-user.ts +├── map-product.ts +└── map-order-to-dto.ts +``` + +## Сегмент types/ + +TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов. + +```text +types/ +├── user.type.ts +└── session.type.ts +``` + +## Сегмент styles/ + +Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.). + +```text +styles/ +├── auth.module.css +└── login-form.module.css +``` + +## Сегмент lib/ + +Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов. + +```text +lib/ +├── validate-email.ts +└── format-phone.ts +``` + +Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`. + +## Сегмент config/ + +Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения. + +```text +config/ +├── routes.ts +└── constants.ts +``` diff --git a/ai/nextjs-style-guide/basics/code-style.md b/ai/nextjs-style-guide/basics/code-style.md new file mode 100644 index 0000000..e47e625 --- /dev/null +++ b/ai/nextjs-style-guide/basics/code-style.md @@ -0,0 +1,153 @@ +--- +title: Стиль кода +description: Как оформляется код в проекте. +--- + +# Стиль кода + +Как оформляется код в проекте. + +## Отступы + +- 2 пробела (не табы). + +## Длина строк + +- Ориентироваться на 100 символов, но превышение допустимо, если строка читается легко. +- Переносить выражение на новые строки, когда строка становится плохо читаемой. +- Не переносить строку внутри строковых литералов без необходимости. + +**Хорошо** +```ts +const config = createRequestConfig( + endpoint, + { + headers: { + 'X-Request-Id': requestId, + 'X-User-Id': userId, + }, + params: { + page, + pageSize, + sort: 'createdAt', + }, + }, + timeoutMs, +); +``` + +**Плохо** +```ts +// Плохо: длинная строка с вложенными структурами плохо читается. +const config = createRequestConfig(endpoint, { headers: { 'X-Request-Id': requestId, 'X-User-Id': userId }, params: { page, pageSize, sort: 'createdAt' } }, timeoutMs); +``` + +## Кавычки + +- В JavaScript/TypeScript использовать одинарные кавычки. +- В JSX/TSX для атрибутов использовать двойные кавычки. +- Шаблонные строки использовать только при интерполяции или многострочном тексте. + +**Хорошо** +```ts +const label = 'Сохранить'; +const title = `Привет, ${name}`; +``` + +```tsx + +``` + +**Плохо** +```ts +// Плохо: двойные кавычки в TS и конкатенация вместо шаблонной строки. +const label = "Сохранить"; +const title = 'Привет, ' + name; +``` + +```tsx +// Плохо: одинарные кавычки в JSX-атрибутах. + +``` + +## Точки с запятой и запятые + +- Допускаются упущения точки с запятой, если код остаётся читаемым и однозначным. +- В многострочных массивах, объектах и параметрах функции запятая в конце допускается, но не обязательна. + +## Импорты + +- В именованных импортах использовать пробелы внутри фигурных скобок. +- Типы импортировать через `import type`. +- `default` экспорт избегать, использовать именованные. `default` импорт допустим (например, стили CSS Modules, сторонние библиотеки). +- Избегать импорта всего модуля через `*`. + +**Хорошо** +```ts +import { MyComponent } from 'MyComponent'; +import type { User } from '../model/types'; +import styles from './styles/button.module.css'; +``` + +**Плохо** +```ts +// Плохо: отсутствие пробелов в именованном импорте. +import type {User} from '../model/types'; +// Плохо: default экспорт. +export default MyComponent; +``` + +## Ранние возвраты (early return) + +- Использовать ранние возвраты для упрощения чтения. +- Избегать `else` после `return`. + +**Хорошо** +```ts +const getName = (user?: { name: string }) => { + if (!user) { + return 'Гость'; + } + + return user.name; +}; +``` + +**Плохо** +```ts +// Плохо: лишний else после return усложняет чтение. +const getName = (user?: { name: string }) => { + if (user) { + return user.name; + } else { + return 'Гость'; + } +}; +``` + +## Форматирование объектов и массивов + +- В многострочных объектах каждое свойство на новой строке. +- В многострочных массивах каждый элемент на новой строке. +- Объекты и массивы можно писать в одну строку, если длина строки не превышает 100 символов. +- В однострочных объектах и массивах использовать пробелы после запятых. + +**Хорошо** +```ts +const roles = ['admin', 'editor', 'viewer']; +const options = { id: 1, name: 'User' }; + +const config = { + url: '/api/users', + method: 'GET', + params: { page: 1, pageSize: 20 }, +}; +``` + +**Плохо** +```ts +// Плохо: нет пробелов после запятых и объект слишком длинный для одной строки. +const roles = ['admin','editor','viewer']; +const options = { id: 1,name: 'User' }; +const config = { url: '/api/users', method: 'GET', params: { page: 1, pageSize: 20 } }; +``` diff --git a/ai/nextjs-style-guide/basics/documentation.md b/ai/nextjs-style-guide/basics/documentation.md new file mode 100644 index 0000000..81abcf1 --- /dev/null +++ b/ai/nextjs-style-guide/basics/documentation.md @@ -0,0 +1,134 @@ +--- +title: Документирование +description: Что и как документировать в коде. +--- + +# Документирование + +Что и как документировать в коде. + +## Общие правила + +- Документировать публичные функции, компоненты, типы, интерфейсы и enum. +- Не документировать очевидное — если название говорит само за себя, комментарий не нужен. +- Не документировать параметры, возвращаемые значения и типы пропсов — они видны из сигнатуры. +- Описание через пользу и назначение, а не через внутреннюю реализацию. +- Описание завершается точкой. + +## Функции + +Для документирования функций используется шаблон. Описание механики опционально — +добавляется когда логика нетривиальна. + +**Шаблон** +```ts +/** + * <Что делает функция в 1 строке>. + * + * <Опционально: описание сложной механики или важных нюансов>. + */ +``` + +**Хорошо** +```ts +/** + * Форматирует цену с символом валюты. + */ +export const formatPrice = (value: number): string => { ... } + +/** + * Рекурсивно собирает дерево категорий из плоского списка. + * + * Группирует элементы по parentId, начиная с корневых (parentId = null). + * Категории без родителя попадают в корень дерева. + */ +export const buildCategoryTree = (categories: Category[]): CategoryTree[] => { ... } +``` + +**Плохо** +```ts +// Плохо: дублирует сигнатуру. +/** + * @param value - число + * @returns строка с ценой + */ +``` + +## Компоненты + +Компонент описывает своё **назначение** и **сценарии применения** — это помогает понять, когда и где его использовать, без необходимости читать реализацию. + +**Шаблон** +```ts +/** + * <Назначение компонента в 1 строке>. + * + * Используется для: + * - <сценарий 1> + * - <сценарий 2> + * - <сценарий 3> + */ +``` + +**Хорошо** +```tsx +/** + * Контейнер с адаптивной максимальной шириной. + * + * Используется для: + * - обёртки контента страниц с ограничением ширины + * - центрирования блоков в лейауте + */ +export const Container = (props: ContainerProps) => { ... } +``` + +**Плохо** +```tsx +// Плохо: описывает реализацию, а не назначение. +/** + * Рендерит div с className и htmlAttr. + */ + +// Плохо: нет описания вообще. +export const Container = (props: ContainerProps) => { ... } +``` + +## Типы, интерфейсы, enum + +Документируются назначение сущности и каждое её поле. + +**Хорошо** +```ts +/** + * Фильтры списка задач. + */ +export enum TodoFilter { + /** Все задачи. */ + ALL = 'all', + /** Только активные. */ + ACTIVE = 'active', + /** Только завершённые. */ + COMPLETED = 'completed', +} + +/** + * Задача пользователя. + */ +export interface TodoItem { + /** Уникальный идентификатор задачи. */ + id: string; + /** Текст задачи. */ + text: string; + /** Статус выполнения. */ + completed: boolean; +} +``` + +**Плохо** +```ts +// Плохо: описывает очевидное. +export interface TodoItem { + /** id — это id */ + id: string; +} +``` diff --git a/ai/nextjs-style-guide/basics/naming.md b/ai/nextjs-style-guide/basics/naming.md new file mode 100644 index 0000000..096dffb --- /dev/null +++ b/ai/nextjs-style-guide/basics/naming.md @@ -0,0 +1,146 @@ +--- +title: Именование +description: Как называть переменные, файлы и прочие сущности в коде. +--- + +# Именование + +Как называть переменные, файлы и прочие сущности в коде. + +## Базовые правила + +| Что | Рекомендуется | +| ---------------- | ---------------------- | +| Папки | `kebab-case` | +| Файлы | `kebab-case` | +| Переменные | `camelCase` | +| Константы | `SCREAMING_SNAKE_CASE` | +| Классы | `PascalCase` | +| React-компоненты | `PascalCase` | +| Хуки | `useSomething` | +| CSS классы | `camelCase` | +| Ключи enum | `SCREAMING_SNAKE_CASE` | + + +## Именование файлов + +Суффикс обозначает роль или тип файла. Пишется в единственном числе. +Формат: `name..ts`. + +**Хуки** +- `use-name.hook.ts` — файл хука, функция именуется `useName` + +**Логика** +- `.store.ts` — стор +- `.service.ts` — сервис + +**Корневые компоненты слоёв** +- `.screen.tsx` — корневой компонент screen-модуля: `screens/profile/profile.screen.tsx`, компонент `ProfileScreen` +- `.layout.tsx` — корневой компонент layout-модуля: `layouts/main/main.layout.tsx`, компонент `MainLayout` + +Обычные и вложенные модули не получают суффикс слоя: `ui/button/button.tsx`, `screens/profile/parts/activity-feed/activity-feed.tsx`. + +**Типы и контракты** +- `.type.ts` — типы и интерфейсы +- `.interface.ts` — интерфейсы +- `.enum.ts` — enum +- `.dto.ts` — внешние DTO +- `.schema.ts` — схемы валидации +- `.constant.ts` — константы +- `.config.ts` — конфигурация + +**Утилиты** +- `.util.ts` — утилиты +- `.helper.ts` — вспомогательные функции +- `.lib.ts` — библиотечный код + +**Тесты** +- `.test.ts` — тесты +- `.mock.ts` — моки + +**Хорошо** +```text +business/ +└── auth-by-email/ + ├── ui/ + │ └── login-form.tsx + ├── hooks/ + │ └── use-auth.hook.ts + ├── stores/ + │ └── auth.store.ts + ├── types/ + │ └── auth.type.ts + ├── auth-by-email.tsx + └── index.ts +``` + +**Плохо** +```text +business/ +└── authByEmail/ + ├── LoginForm.tsx + ├── useAuth.ts + ├── authStore.ts + └── index.ts +``` + +## Булевы значения + +- Использовать префиксы `is`, `has`, `can`, `should`. + +**Хорошо** +```ts +const isReady = true; +const hasAccess = false; +const canSubmit = true; +const shouldRedirect = false; +``` + +**Плохо** +```ts +// Плохо: неясное булево значение без префикса. +const ready = true; +const access = false; +const submit = true; +``` + +## События и обработчики + +- Обработчики начинать с `handle`. +- События и колбэки начинать с `on`. + +**Хорошо** +```ts +const handleSubmit = () => { ... }; +const onSubmit = () => { ... }; +``` + +**Плохо** +```ts +// Плохо: неочевидное назначение имени. +const submitClick = () => { ... }; +``` + +## Коллекции + +- Для массивов использовать имена во множественном числе. +- Для словарей/мап — использовать суффиксы `ById`, `Map`, `Dict`. + +**Хорошо** +```ts +const users = []; +const usersById = {} as Record; +const userIds = ['u1', 'u2']; +const ordersMap = new Map(); +const featureFlagsDict = { beta: true, legacy: false } as Record; +``` + +**Плохо** +```ts +// Плохо: имя не отражает, что это коллекция. +const user = []; +// Плохо: словарь назван как массив. +const usersMap = []; +// Плохо: по имени непонятно, что это словарь. +const users = {} as Record; +``` diff --git a/ai/nextjs-style-guide/basics/tech-stack.md b/ai/nextjs-style-guide/basics/tech-stack.md new file mode 100644 index 0000000..484ab93 --- /dev/null +++ b/ai/nextjs-style-guide/basics/tech-stack.md @@ -0,0 +1,42 @@ +--- +title: Технологии и библиотеки +description: Какие библиотеки и инструменты используются в проекте. +--- + +# Технологии и библиотеки + +Какие библиотеки и инструменты используются в проекте. + +## Что используем + +### Стек +- `React` / `TypeScript` — основной стек для UI и приложения. +- `Next.js` — для продуктовых сайтов. + +### Архитектура +- `SLM Design` — собственная модульная архитектура проекта. Подробнее в разделе [Архитектура](./architecture/index.md). + +### UI компоненты +- `Mantine UI` — базовые UI-компоненты. + +### Работа с данными (API) +- `@gromlab/api-codegen` — генерация API‑клиентов и типов. +- `SWR` — получение, кеширование, ревалидация, дедубликация. +- `SWR (useSWRSubscription)` — сокеты, реалтайм подписки. + +### Store +- `Zustand` — глобальное состояние. + +### Локализация +- `i18next (i18n)` — локализация всех пользовательских текстов. + +### Тестирование +- `Vitest` — тестирование. + +### Стили +- `PostCSS Modules` — изоляция стилей. +- `Mobile First` — подход к адаптивной верстке. +- `clsx` — конкатенация CSS‑классов. + +### Генерация +- `@gromlab/create` — шаблонизатор для создания слоёв и других файлов из шаблонов. diff --git a/ai/nextjs-style-guide/basics/typing.md b/ai/nextjs-style-guide/basics/typing.md new file mode 100644 index 0000000..c14743d --- /dev/null +++ b/ai/nextjs-style-guide/basics/typing.md @@ -0,0 +1,57 @@ +--- +title: Типизация +description: Как типизируется код в проекте. +--- + +# Типизация + +Как типизируется код в проекте. + +## Общие правила + +- Указывать типы для параметров компонентов, возвращаемых значений и параметров функций. +- Предпочитать `type` для описания сущностей и `interface` для расширяемых контрактов. +- Избегать `any` и `unknown` без необходимости. +- Не использовать `ts-ignore`, кроме крайних случаев с явным комментарием причины. + +## Функции + +- Для публичных функций указывать возвращаемый тип. +- Не полагаться на неявный вывод для важных API. + +**Хорошо** +```ts +export const formatPrice = (value: number): string => { + return `${value} ₽`; +}; +``` + +**Плохо** +```ts +// Плохо: нет явного возвращаемого типа. +export const formatPrice = (value: number) => { + return `${value} ₽`; +}; +``` + +## Работа с any/unknown + +- `any` использовать только для временных заглушек. +- `unknown` сужать через проверки перед использованием. + +**Хорошо** +```ts +const parse = (value: unknown): string => { + if (typeof value === 'string') { + return value; + } + + return ''; +}; +``` + +**Плохо** +```ts +// Плохо: any отключает проверку типов. +const parse = (value: any) => value; +``` diff --git a/ai/nextjs-style-guide/creating-project/from-template.md b/ai/nextjs-style-guide/creating-project/from-template.md new file mode 100644 index 0000000..c549640 --- /dev/null +++ b/ai/nextjs-style-guide/creating-project/from-template.md @@ -0,0 +1,51 @@ +--- +title: Создание проекта из шаблона +description: Создание нового проекта на основе готового шаблона. +keywords: [создать проект из шаблона, шаблон, template, tiged, degit, клонировать шаблон, эталонный шаблон, быстрый старт, scaffold, новый проект] +--- + +# Создание проекта из шаблона + +Создание нового проекта на основе готового шаблона. + +## Что внутри + +Шаблон — готовый скелет проекта с применёнными правилами стайлгайда: + +- **Стек:** Next.js (App Router), TypeScript, React. +- **Архитектура:** структура папок по SLM, алиасы импортов. +- **Качество кода:** Biome (линтер и форматер), настройки VS Code. +- **Стили:** PostCSS Modules с плагинами, токены, медиа-брейкпоинты. +- **Ассеты:** генерация SVG-спрайтов. +- **Кодогенерация:** шаблоны для страниц, компонентов, хуков, сторов. +в +## Установка + +1. Склонировать шаблон в родительском каталоге будущего проекта: + + ```bash + npx tiged git@gromlab.ru:templates/nextjs.git my-app + ``` + + `tiged` копирует снимок репозитория без истории git. Имя каталога (`my-app`) заменяется на нужное. + +2. Установить зависимости: + + ```bash + cd my-app + npm install + ``` + +3. Проверить сборку: + + ```bash + npm run build + ``` + + Сборка должна завершиться без ошибок. + +## Правила + +- **Шаблон — источник истины.** Не добавлять, не удалять и не переименовывать файлы шаблона «для приведения к канону»: шаблон уже канонический. Любое несоответствие — баг шаблона, а не проекта. +- **Менеджер пакетов — npm.** Отклонение (pnpm, yarn, bun) — только по явному решению с пониманием, что стайлгайд этого не предусматривает. +- **Не инициализировать git заново** автоматически. `tiged` намеренно не создаёт `.git/` — решение о репозитории принимает разработчик. diff --git a/ai/nextjs-style-guide/creating-project/manual.md b/ai/nextjs-style-guide/creating-project/manual.md new file mode 100644 index 0000000..fb89536 --- /dev/null +++ b/ai/nextjs-style-guide/creating-project/manual.md @@ -0,0 +1,90 @@ +--- +title: Создание проекта вручную +description: Поэтапное создание нового проекта без использования шаблона. +keywords: [создать проект, новый проект, с нуля, init, initialize, scaffold, create-next-app, начать проект, поднять проект, эталонный проект, ручная установка] +--- + +# Создание проекта вручную + +Поэтапное создание нового проекта без использования шаблона. + +## Состав эталонного проекта + +| Компонент | Роль | Раздел | +|-----------|------|--------| +| Next.js | Фреймворк (роутинг, сборка, SSR) | [Next.js](./nextjs.md) | +| Алиасы | Импорты по слоям SLM | [Алиасы](../applied/aliases.md) | +| Biome | Линтер и форматтер (замена ESLint + Prettier) | [Biome](../applied/biome.md) | +| Стили | Глобальные токены и breakpoints | [Стили](../applied/styles/styles-setup.md) | +| PostCSS | CSS-процессор для custom-media и вложенности | [PostCSS](../applied/postcss.md) | +| SVG-спрайты | Иконки через ``, управление цветом | [SVG-спрайты](../applied/svg-sprites/svg-sprites-setup.md) | +| VS Code | Настройки редактора и расширения | [VS Code](../applied/vscode.md) | +| Шаблоны генерации | `.templates/` для `@gromlab/create` | [Шаблоны генерации](../applied/templates/templates-setup.md) | + +Убрать компонент из состава — значит согласованно отказаться от части стайлгайда. Частичные проекты возможны (только Next.js, Next.js + стили и т.п.), но не являются эталоном. + +## Канон раскладки + +В `src/` допустимы только слои SLM: `app/`, `layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](../applied/project-structure.md), [Архитектура](../basics/architecture/index.md)). + +В частности: `src/app/` содержит только файлы роутинга Next.js и инициализации, без каталогов `styles/`, `assets/`, `components/`. + +## Порядок установки + +Подсистемы ставятся в фиксированном порядке — он отражает зависимости между шагами. + +### 1. Next.js + +Скелет фреймворка — обязательный первый шаг, остальное опирается на него. + +См. [Next.js](./nextjs.md). После выполнения проверки этого раздела `npm run build` должен проходить. + +### 2. Алиасы + +Заменить дефолтный `"@/*"` в `tsconfig.json` на канонический список из восьми слой-префиксов. + +См. [Алиасы](../applied/aliases.md). + +### 3. Biome + +Линтер и форматтер. Подключается **до** написания кода, иначе в проекте копятся несогласованные правки. + +См. [Biome](../applied/biome.md). + +### 4. Стили (базовая инфраструктура) + +Файлы `variables.css`, `media.css`, `global.css` в `src/shared/styles/` и подключение `global.css` в `src/app/layout.tsx`. CSS-процессор на этом шаге не ставится. + +См. [Стили](../applied/styles/styles-setup.md). + +### 5. PostCSS + +CSS-процессор поверх базовых стилей: `@custom-media`, вложенность, autoprefixer. Ставится **только после шага 4** — опирается на `src/shared/styles/media.css`. + +См. [PostCSS](../applied/postcss.md). + +### 6. SVG-спрайты + +Пакет `@gromlab/svg-sprites`, генерация спрайт-файла и React-компонента ``. + +См. [SVG-спрайты](../applied/svg-sprites/svg-sprites-setup.md). + +### 7. VS Code + +Расширения и настройки редактора. Опирается на установленный Biome (форматирование при сохранении) и PostCSS (ассоциация `*.css`). + +См. [VS Code](../applied/vscode.md). + +### 8. Шаблоны генерации + +Папка `.templates/` для генератора модулей `@gromlab/create`. + +См. [Шаблоны генерации](../applied/templates/templates-setup.md). + +## Правила + +- **Порядок шагов фиксирован.** Перестановка ломает зависимости (PostCSS требует базовых стилей, VS Code — установленного Biome). +- **Между шагами обязательна проверка** из соответствующего раздела. Не переходить дальше, пока чеклист текущего шага не пройден. +- **Слои `src/`** (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`) не создавать авансом. Появляются по мере первого модуля. Исключения — `src/app/` (создаётся `create-next-app`), `src/shared/styles/` (шаг 1) и `src/shared/sprites/icons/` (шаг 6). +- **Посторонние каталоги в `src/`** (`assets/`, `utils/`, `lib/`, `components/` и т.п.) — запрещены. +- **Подмножество шагов допустимо.** Можно ставить только Next.js и часть инструментов; полный набор — это эталон, а не обязательство. diff --git a/ai/nextjs-style-guide/creating-project/nextjs.md b/ai/nextjs-style-guide/creating-project/nextjs.md new file mode 100644 index 0000000..dc3b9d7 --- /dev/null +++ b/ai/nextjs-style-guide/creating-project/nextjs.md @@ -0,0 +1,112 @@ +--- +title: Чистая установка Next.js +description: "Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку." +keywords: [next.js, create-next-app, npx, установка, инициализация, фреймворк, скаффолдинг, app router, typescript] +--- + +# Чистая установка Next.js + +Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку. + +## Требования + +- Node.js 18.18+ (рекомендуется LTS 20+). +- npm 10+. +- Рабочая папка пуста, либо для установки выбрана подпапка (`create-next-app@16+` отказывается ставиться в непустую директорию). + +## Установка + +### 1. Инициализация через `create-next-app` + +Флаги зафиксированы и не согласовываются — это канон стайлгайда: + +```bash +npx create-next-app@latest my-app \ + --typescript \ + --app \ + --src-dir \ + --import-alias "@/*" \ + --no-eslint \ + --no-tailwind \ + --use-npm +``` + +| Флаг | Значение | Почему так | +|------|----------|------------| +| `--typescript` | TS включён | Стайлгайд требует TypeScript ([Типизация](../basics/typing.md)) | +| `--app` | App Router | Pages Router не используется | +| `--src-dir` | Код в `src/` | Архитектура SLM требует `src/` ([Структура проекта](../applied/project-structure.md)) | +| `--import-alias "@/*"` | Placeholder | Требуется флагом; после установки `paths` полностью переписывается на слой-префиксы (см. [Алиасы](../applied/aliases.md)) | +| `--no-eslint` | ESLint не ставится | Линтер и форматтер — Biome ([Biome](../applied/biome.md)) | +| `--no-tailwind` | Tailwind не ставится | Стилизация — PostCSS Modules ([Стили](../applied/styles/styles-usage.md)) | +| `--use-npm` | Пакетный менеджер — npm | Единый инструмент в проектах | + +### 2. Очистить дефолтный шаблон + +`create-next-app` генерирует демо-страницу со стилями и ассетами, а Next.js 16+ дополнительно кладёт в корень собственные `AGENTS.md` и `CLAUDE.md` — всё это удаляется. + +```bash +rm src/app/page.module.css +rm src/app/globals.css +rm public/next.svg public/vercel.svg public/file.svg public/globe.svg public/window.svg +rm -f AGENTS.md CLAUDE.md +``` + +Заменить `src/app/page.tsx` на минимальный: + +```tsx +// src/app/page.tsx +export default function HomePage() { + return

Home

+} +``` + +Очистить `src/app/layout.tsx` от импорта шрифтов и `globals.css`: + +```tsx +// src/app/layout.tsx +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'App', + description: '', +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} +``` + +### 3. Создать папку `src/shared/styles/` + +Глобальные стили в SLM-архитектуре живут в слое `shared`, а не в `src/app/` ([Структура проекта](../applied/project-structure.md)). + +```bash +mkdir -p src/shared/styles +``` + +Остальные слои (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`) заводятся при появлении первого модуля в них. `src/shared/styles/` — единственный подкаталог `shared/`, который заводится сразу: без него не настроить стили на следующих шагах. + +## Правила + +- **Конфликт с непустой директорией** — не удалять файлы пользователя автоматически. Ставить в подпапку или временно перенести посторонние файлы. +- **Отклонение от канонических флагов** (pnpm, Tailwind, ESLint и т.п.) — только осознанное, с пониманием, что стайлгайд этого не предусматривает. +- **Слои `src/`** не создавать авансом — появляются при первом модуле. Алиасы прописываются сразу на все восемь слоёв (см. [Алиасы](../applied/aliases.md)). +- **`AGENTS.md` от Next.js** удаляется в шаге 2. Повторно не создаётся. +- **Biome, стили, PostCSS, SVG-спрайты, VS Code** — отдельные шаги установки, не в этом разделе. + +## Проверка установки + +- В корне проекта: `next.config.ts`, `tsconfig.json`, `package.json`. +- В `package.json`: Next.js установлен, нет `eslint`, `tailwindcss`. +- В `src/app/` присутствуют минимальные `page.tsx` и `layout.tsx`. `globals.css`, `page.module.css` отсутствуют. Каталогов `styles/`, `assets/`, `providers/`, `components/` в `src/app/` нет. +- Папка `src/shared/styles/` создана (пустая). +- В `src/` из слоёв SLM присутствуют только `app/` и `shared/` (с `styles/`). Посторонних каталогов нет. +- В `public/` удалены `next.svg`, `vercel.svg`, `file.svg`, `globe.svg`, `window.svg`. +- В корне проекта нет `AGENTS.md` и `CLAUDE.md` от Next.js. +- `npm run build` завершается успешно. +- Пакетный менеджер — npm (нет `pnpm-lock.yaml`, `yarn.lock`, `bun.lockb`). diff --git a/ai/nextjs-style-guide/data/index.md b/ai/nextjs-style-guide/data/index.md new file mode 100644 index 0000000..906a5be --- /dev/null +++ b/ai/nextjs-style-guide/data/index.md @@ -0,0 +1,60 @@ +--- +title: Источники данных +description: Какие источники данных используются в проекте и как с ними работать. +keywords: [данные, api, rest, realtime, клиент, swr, infrastructure, введение, карта раздела] +--- + +# Источники данных + +Какие источники данных используются в проекте и как с ними работать. + +## Принципы раздела + +- **Клиент — в `infrastructure/`.** Каждый внешний сервис — отдельный модуль слоя `infrastructure/{service-name}/`. +- **Прямой `fetch` запрещён.** Запросы идут только через клиент модуля. Исключения — точечные и обоснованные. +- **Источник данных диктует канал.** REST, realtime и т.п. — независимые подразделы, у каждого своя модель клиента и своё потребление. +- **Серверные и клиентские компоненты потребляют по-разному.** Server Components — прямой `await` метода клиента, клиентские — через готовые GET-хуки REST-клиента (`useGetUserList`, `useGetPostDetail` и т.п.). SWR инкапсулирован в хуке, компонент про него не знает. + +## Карта раздела + +### REST + +Канал «запрос-ответ» по HTTP. Покрывает большинство API. + +- [REST](./rest/index.md) — обзор раздела: создание клиента и использование. +- **Создание клиента** — как оформляется REST API в проекте: + - [Обзор](./rest/clients/index.md) — когда нужен клиент и как выбрать подход. + - [Автогенерация из OpenAPI](./rest/clients/auto.md) — для API с OpenAPI-спецификацией, через `@gromlab/api-codegen`. + - [Ручное создание](./rest/clients/manual.md) — для API без схемы, клиент пишется и поддерживается руками. + - [GET-хуки REST-клиента](./rest/clients/hooks.md) — прозрачные SWR-обёртки над GET-методами клиента. +- **Использование** — как получать данные через готовый клиент: + - [Стратегии получения данных](./rest/strategies/index.md) — как выбрать способ получения данных под ситуацию. + - [Серверный await](./rest/strategies/server-await.md) — прямой `await` метода клиента в Server Components. + - [Параллельные серверные запросы](./rest/strategies/parallel-server-requests.md) — запуск независимых серверных запросов без waterfall. + - [Передача промиса ниже](./rest/strategies/pass-promise-down.md) — серверный стриминг через промис и `Suspense`. + - [Начальные данные для клиентских хуков](./rest/strategies/client-hooks-initial-data.md) — серверный промис в `SWRConfig fallback`. + - [Клиентский GET-хук](./rest/strategies/client-get-hook.md) — получение данных в Client Components через готовый GET-хук. + - [Business-композиция](./rest/strategies/business-composition.md) — доменная интерпретация и композиция REST-данных. + +### Realtime + +Канал push-данных: WebSocket, SSE, событийные шины. Транспорт не зашит в правила — важна абстракция «подписка». + +- [Realtime](./realtime.md) — клиент realtime в `infrastructure/`, потребление через `useSWRSubscription` или прямые подписки. + +## Что даёт раздел + +После прочтения раздела понятно: + +- Где живёт код работы с API и почему именно там. +- Когда генерировать клиент автоматически, а когда писать вручную, и как структурирован каждый из вариантов. +- Какие GET-хуки относятся к REST-клиенту и почему они живут в `infrastructure/{service-name}/hooks/`. +- Как выбрать стратегию получения REST-данных под конкретную ситуацию. +- Как подключать realtime-источники в общую модель работы с данными. +- Какие правила обязательны и какие отклонения допустимы. + +## Что не входит в раздел + +- **Глобальное состояние UI** — Stores, формы, фичефлаги. Это [Stores](../applied/stores.md). +- **Доменная логика** — как данные превращаются в сценарии бизнеса. Это слой `business/` в [Архитектуре](../basics/architecture/index.md). +- **Хуки общего назначения** — переиспользуемые хуки UI, не привязанные к конкретному API. Отдельный прикладной раздел для них пока не ведётся. diff --git a/ai/nextjs-style-guide/data/realtime.md b/ai/nextjs-style-guide/data/realtime.md new file mode 100644 index 0000000..7463cfe --- /dev/null +++ b/ai/nextjs-style-guide/data/realtime.md @@ -0,0 +1,79 @@ +--- +title: Realtime +description: "Работа с push-данными от сервера: подписки и события." +keywords: [realtime, websocket, sse, подписка, swr subscription, useSWRSubscription, push, события] +--- + +# Realtime + +Работа с push-данными от сервера: подписки и события. + +## Принципы + +- **Клиент realtime — в `infrastructure/`** отдельным модулем по имени канала. То же правило, что и для REST: никаких прямых соединений в коде приложения. +- **Подписка — единица потребления.** Клиент даёт функцию `subscribe(topic, handler) → unsubscribe`. Внутри — конкретный транспорт. +- **Использование на клиенте — два сценария:** + - **`useSWRSubscription`** — для данных, которые показываются в UI и должны кешироваться/синхронизироваться с REST. + - **Прямая подписка** — для побочных эффектов (тосты, нотификации, аналитика), не привязанных к рендеру. + +## Размещение клиента + +```text +src/infrastructure/ +└── {channel-name}/ + ├── connection.ts # установление соединения, реконнект + ├── subscribe.ts # subscribe(topic, handler) → unsubscribe + ├── types.ts + └── index.ts +``` + +## Использование через SWR + +```tsx +'use client' + +import useSWRSubscription from 'swr/subscription' +import { subscribe } from 'infrastructure/notifications' + +export function NotificationCounter() { + const { data: count } = useSWRSubscription( + ['notifications', 'count'], + (key, { next }) => + subscribe('notifications.count', (value: number) => next(null, value)), + ) + + return {count ?? 0} +} +``` + +Плюсы: кеш и дедупликация подписки между несколькими местами рендера; единая модель данных с REST. + +## Прямая подписка + +Для побочных эффектов, которые не влияют на состояние UI напрямую: + +```tsx +'use client' + +import { useEffect } from 'react' +import { subscribe } from 'infrastructure/notifications' +import { showToast } from 'ui/toast' + +export function NotificationsToaster() { + useEffect(() => { + return subscribe('notifications.new', (notification) => { + showToast(notification.message) + }) + }, []) + + return null +} +``` + +Возврат `unsubscribe` из `useEffect` обязателен — иначе утечка подписки. + +## Запрет прямых соединений + +Создавать `new WebSocket(...)`, `new EventSource(...)` или подписываться на событийные шины напрямую в коде приложения — запрещено. Все соединения проходят через клиент в `infrastructure/`. + +Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием. diff --git a/ai/nextjs-style-guide/data/rest/clients/auto.md b/ai/nextjs-style-guide/data/rest/clients/auto.md new file mode 100644 index 0000000..2d4b06a --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/clients/auto.md @@ -0,0 +1,193 @@ +--- +title: Автогенерация из OpenAPI +description: Генерация REST-клиента из OpenAPI-спецификации через @gromlab/api-codegen. +keywords: [rest, openapi, api-codegen, автогенерация, generated, npx] +--- + +# Автогенерация из OpenAPI + +Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки. + +## Пример API + +В примерах используется Swagger Petstore: + +```text +https://petstore3.swagger.io/api/v3/openapi.json +``` + +Имена модуля: + +```text +src/infrastructure/pet-store-api/ +petStoreApi +pet-store-api.generated.ts +``` + +## Скрипт генерации + +`@gromlab/api-codegen` не устанавливается в `devDependencies`. Используем `npx @gromlab/api-codegen@latest`, чтобы запускать свежую версию. + +```json +{ + "scripts": { + "codegen:pet-store-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infrastructure/pet-store-api/generated -n pet-store-api.generated" + } +} +``` + +Параметры: + +- `-i` — путь к OpenAPI-спецификации: URL или локальный файл. +- `-o` — директория для сгенерированного файла. +- `-n` — имя сгенерированного файла без `.ts`. + +Ключ `--swr` не используется. GET-хуки REST-клиента пишутся вручную, чтобы сохранить проектный контракт: один GET-хук = один GET-метод, без бизнес-логики и композиции. + +## Генерация + +```bash +npm run codegen:pet-store-api +``` + +Ожидаемый результат: + +```text +src/infrastructure/pet-store-api/generated/ +└── pet-store-api.generated.ts +``` + +Сгенерированный файл не правится руками и коммитится в репозиторий. + +## Проверка методов + +После генерации откройте `generated/pet-store-api.generated.ts` и проверьте фактические имена методов. + +Для Petstore нужны GET-операции вида: + +```ts +petStoreApi.pet.findPetsByStatus(...) +petStoreApi.pet.getPetById(...) +``` + +Точные сигнатуры зависят от OpenAPI-схемы и версии генератора. В рабочих задачах всегда сверяйтесь с generated-файлом. + +## `client.ts` + +Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента. + +```ts +// src/infrastructure/pet-store-api/client.ts +import { Api, HttpClient } from './generated/pet-store-api.generated' + +const httpClient = new HttpClient({ + baseUrl: 'https://petstore3.swagger.io/api/v3', + baseApiParams: { + secure: false, + headers: { + 'Content-Type': 'application/json', + }, + }, +}) + +export const petStoreApi = new Api(httpClient) +``` + +В реальном проекте вместо строки `baseUrl` обычно берётся из env-переменной, нормализуется в `client.ts` и передаётся в `HttpClient` через конфиг. + +`client.ts` не содержит расширения типов, `declare module` и `Extended`-типы. Он только настраивает транспорт и экспортирует инстанс клиента. + +## Расширение сгенерированных типов + +Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`. + +```text +src/infrastructure/biocad-less-api/ +├── generated/ +│ └── biocad-less-api.generated.ts +├── types/ +│ ├── term.ts +│ └── index.ts +├── client.ts +└── index.ts +``` + +Пример расширения generated-типа: + +```ts +// src/infrastructure/biocad-less-api/types/term.ts +import type { TermRecordItem } from '../generated/biocad-less-api.generated' + +declare module '../generated/biocad-less-api.generated' { + interface TermRecordItem { + media?: { + file?: string + title?: string + url?: string + } + } +} + +export type TermRecordItemExtended = Omit< + TermRecordItem, + 'categories' | 'tags' | 'fields' +> & { + categories?: Array<{ + _id?: string + id?: string + slug?: string + name?: string + }> + tags?: Array<{ + _id?: string + id?: string + slug?: string + name?: string + }> + fields?: Record +} +``` + +```ts +// src/infrastructure/biocad-less-api/types/index.ts +export type { TermRecordItemExtended } from './term' +``` + +`declare module` используется для добавления отсутствующих полей в generated-интерфейс. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файл. + +## Публичный API + +```ts +// src/infrastructure/pet-store-api/index.ts +export { petStoreApi } from './client' +export type { Pet } from './generated/pet-store-api.generated' +export * from './hooks' +``` + +Наружу импортируют только из `infrastructure/pet-store-api`, не из `generated/`. + +Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`: + +```ts +// src/infrastructure/biocad-less-api/index.ts +export type { TermRecordItemExtended } from './types' +``` + +## Регенерация + +При изменении OpenAPI-схемы: + +```bash +npm run codegen:pet-store-api +``` + +Что меняется: + +- `generated/pet-store-api.generated.ts` — перезаписывается генератором. +- `client.ts`, `hooks/`, `types/`, `index.ts` — не трогаются автоматически. + +Если после регенерации поменялись сигнатуры методов или типы, это исправляется в ручном коде модуля. + +## Следующий шаг + +После генерации и настройки `client.ts` проверьте серверный вызов метода клиента или добавьте [GET-хук REST-клиента](./hooks.md) для Client Components. diff --git a/ai/nextjs-style-guide/data/rest/clients/hooks.md b/ai/nextjs-style-guide/data/rest/clients/hooks.md new file mode 100644 index 0000000..83dbaae --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/clients/hooks.md @@ -0,0 +1,206 @@ +--- +title: GET-хуки REST-клиента +description: Прозрачные SWR-обёртки над GET-методами REST-клиента. +keywords: [rest, swr, get-хуки, client components, infrastructure] +--- + +# GET-хуки REST-клиента + +GET-хуки REST-клиента — прозрачные SWR-обёртки над GET-методами API-клиента. Они нужны, чтобы Client Components получали данные с кешированием, дедупликацией и ревалидацией, не работая с `useSWR` напрямую. + +## Где лежат + +GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним: + +```text +src/infrastructure/ +└── pet-store-api/ + ├── client.ts + ├── generated/ + ├── hooks/ + │ ├── use-get-pet-list.hook.ts + │ ├── use-get-pet-detail.hook.ts + │ └── index.ts + └── index.ts +``` + +## Контракт + +- Один GET-хук = один GET-метод клиента. +- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`. +- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`. +- Хук принимает только параметры GET-метода и `config?: SWRConfiguration`. +- Что передали хуку, то он передаёт в GET-метод. +- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`. +- Хук возвращает тип ответа API: generated-тип или DTO из `types/`. +- Хук не объединяет несколько запросов. +- Хук не маппит DTO в доменную модель. +- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`. +- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние. + +## Пример списка + +```ts +// src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts +import useSWR from 'swr' +import type { SWRConfiguration } from 'swr' +import { petStoreApi } from '../client' +import type { Pet } from '../generated/pet-store-api.generated' + +export type PetStatus = 'available' | 'pending' | 'sold' + +export const getPetListKey = (status: PetStatus) => + ['pet-store-api', 'pet', 'list', status] as const + +/** + * Получение списка питомцев по статусу. + */ +export const useGetPetList = (status: PetStatus | null, config?: SWRConfiguration) => { + const isReady = status !== null + const key = isReady ? getPetListKey(status) : null + const fetcher = () => petStoreApi.pet.findPetsByStatus({ status }) + + return useSWR(key, fetcher, config) +} +``` + +Функция `getPetListKey` нужна, чтобы один и тот же SWR-ключ использовался внутри GET-хука и при передаче начальных данных через `SWRConfig fallback`. + +Пример начальных данных для клиентского хука: + +```tsx +import type { ReactNode } from 'react' +import { SWRConfig, unstable_serialize } from 'swr' +import { + getPetListKey, + petStoreApi, +} from 'infrastructure/pet-store-api' + +export default function PetsLayout({ children }: { children: ReactNode }) { + const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' }) + + return ( + + {children} + + ) +} +``` + +Клиентский компонент при этом ничего не знает про preload/fallback и продолжает вызывать обычный хук: + +```tsx +const { data: pets } = useGetPetList('available') +``` + +## Пример detail-запроса + +```ts +// src/infrastructure/pet-store-api/hooks/use-get-pet-detail.hook.ts +import useSWR from 'swr' +import type { SWRConfiguration } from 'swr' +import { petStoreApi } from '../client' +import type { Pet } from '../generated/pet-store-api.generated' + +export const getPetDetailKey = (id: number) => + ['pet-store-api', 'pet', 'detail', id] as const + +/** + * Получение питомца по идентификатору. + */ +export const useGetPetDetail = (id: number | null, config?: SWRConfiguration) => { + const isReady = id !== null + const key = isReady ? getPetDetailKey(id) : null + const fetcher = () => petStoreApi.pet.getPetById(id) + + return useSWR(key, fetcher, config) +} +``` + +## Отложенный запрос через `null` + +GET-хук может принимать `null` для обязательного параметра. `null` означает, что параметр ещё не готов и запрос выполнять нельзя. + +Внутри хука это выражается через `isReady`: если параметр не готов, ключ SWR становится `null`, и SWR не вызывает fetcher. + +```ts +const isReady = id !== null +const key = isReady ? getPetDetailKey(id) : null +``` + +`null` не передаётся в метод клиента. Key-функция принимает только готовые параметры, поэтому её можно безопасно использовать для начальных данных через `SWRConfig fallback`. + +Для числовых идентификаторов не используйте проверку `if (id)`: значение `0` тоже валидное число. Проверяйте явно: `id !== null`. + +## Экспорт + +```ts +// src/infrastructure/pet-store-api/hooks/index.ts +export { getPetListKey, useGetPetList } from './use-get-pet-list.hook' +export type { PetStatus } from './use-get-pet-list.hook' +export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook' +``` + +```ts +// src/infrastructure/pet-store-api/index.ts +export { petStoreApi } from './client' +export type { Pet } from './generated/pet-store-api.generated' +export * from './hooks' +``` + +## Где заканчивается infrastructure + +```ts +// Хорошо: infrastructure, прозрачный GET-хук +const { data: pets } = useGetPetList('available') +``` + +```ts +// Хорошо: business, доменная интерпретация +export const useAvailablePets = () => { + const query = useGetPetList('available') + + return { + ...query, + hasPets: Boolean(query.data?.length), + } +} +``` + +`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`. + +## Что запрещено + +```ts +// Плохо — useSWR в компоненте +const { data } = useSWR( + ['pet-store-api', 'pet', 'list', status], + () => petStoreApi.pet.findPetsByStatus({ status }), +) + +// Плохо — несколько GET внутри infrastructure-хука +export const usePetDashboard = () => { + const available = useGetPetList('available') + const sold = useGetPetList('sold') + + return { available, sold } +} + +// Плохо — бизнес-флаг внутри GET-хука REST-клиента +export const useGetPetList = (status: PetStatus) => { + const query = useSWR(...) + + return { + ...query, + hasPets: Boolean(query.data?.length), + } +} +``` + +Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](../strategies/client-get-hook.md). diff --git a/ai/nextjs-style-guide/data/rest/clients/index.md b/ai/nextjs-style-guide/data/rest/clients/index.md new file mode 100644 index 0000000..9e2a601 --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/clients/index.md @@ -0,0 +1,75 @@ +--- +title: Создание клиента +description: Из чего состоит REST-клиент и какие части нужно подготовить перед использованием API. +keywords: [rest, клиент, infrastructure, методы, openapi, get-хуки, swr] +--- + +# Создание клиента + +REST-клиент — это infrastructure-модуль, через который проект работает с внешним REST API. + +На этом этапе нужно подготовить клиент сервиса: создать оболочку клиента, получить методы API и добавить GET-хуки для клиентских компонентов. + +## Из чего состоит клиент + +REST-клиент состоит из трёх основных частей: + +1. **Клиент** — самописная оболочка над транспортом. +2. **Методы** — сгенерированные из OpenAPI или написанные вручную вызовы API. +3. **GET-хуки** — SWR-обёртки для GET-запросов. + +Эти части живут в одном REST-модуле, потому что относятся к одному внешнему сервису. + +## Клиент + +Клиент — ручной слой, который настраивает работу с API: `baseUrl`, заголовки, авторизацию, обработку ошибок и создание инстанса сервиса. + +Даже если методы генерируются из OpenAPI, `client.ts` остаётся ручным файлом проекта. + +`client.ts` — только сборочная точка клиента. В нём не размещаются DTO, `declare module`, `Extended`-типы, GET-хуки и бизнес-логика. + +## Методы + +Методы описывают конкретные запросы к API. + +Они появляются одним из двух способов: + +- генерируются из OpenAPI в `generated/`; +- создаются вручную в `methods/`. + +Подробности: + +- [Автогенерация из OpenAPI](./auto.md) +- [Ручное создание](./manual.md) + +## GET-хуки + +Для GET-запросов добавляются GET-хуки REST-клиента. + +Это прозрачные SWR-обёртки над GET-методами клиента. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components. + +GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`. + +Подробности: + +- [GET-хуки REST-клиента](./hooks.md) + +## Структура модуля + +```text +src/infrastructure/{service-name}/ +├── client.ts # самописная оболочка и инстанс клиента +├── generated/ или methods/ # методы API +├── hooks/ # GET-хуки REST-клиента +├── types/ # DTO, типы API и расширения типов +├── errors/ # ошибки API, если нужны +└── index.ts # публичный API +``` + +`index.ts` — единственная точка входа в REST-модуль для внешнего кода. + +## Что делаем дальше + +1. Создайте методы клиента: [Автогенерация из OpenAPI](./auto.md) или [Ручное создание](./manual.md). +2. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](./hooks.md). +3. После создания клиента переходите к [Стратегиям получения данных](../strategies/index.md). diff --git a/ai/nextjs-style-guide/data/rest/clients/manual.md b/ai/nextjs-style-guide/data/rest/clients/manual.md new file mode 100644 index 0000000..f5545dc --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/clients/manual.md @@ -0,0 +1,187 @@ +--- +title: Ручное создание +description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный. +keywords: [rest, ручной клиент, fetch, methods, dto, errors, infrastructure] +--- + +# Ручное создание + +Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации. + +Задача ручного клиента — дать такую же точку входа, как у автогенерированного клиента: именованный API-объект, методы по сущностям, DTO и GET-хуки рядом с клиентом. + +## Что нужно создать + +```text +src/infrastructure/ +└── pet-project-api/ + ├── methods/ + │ └── posts.ts + ├── hooks/ + │ └── index.ts + ├── types/ + │ ├── client.ts + │ ├── post.ts + │ └── index.ts + ├── errors/ + │ └── pet-project-api.error.ts + ├── client.ts + └── index.ts +``` + +| Файл | Роль | +|------|------| +| `client.ts` | Базовый транспорт и создание инстанса клиента | +| `methods/` | Методы API по сущностям | +| `types/` | DTO запросов, ответов и типы клиента | +| `errors/` | Ошибки конкретного API | +| `hooks/` | GET-хуки REST-клиента, если данные нужны в Client Components | +| `index.ts` | Публичный API REST-модуля | + +## DTO и типы API + +DTO запросов и ответов живут в `types/`. `client.ts` не содержит DTO и доменные типы. + +```ts +// src/infrastructure/pet-project-api/types/post.ts +export type PostDto = { + id: string + slug: string + title: string +} + +export type PostListQueryDto = { + limit?: number + category?: string +} +``` + +```ts +// src/infrastructure/pet-project-api/types/index.ts +export type { PostDto, PostListQueryDto } from './post' +``` + +Типы, которые нужны только базовому транспорту, можно держать отдельно: + +```ts +// src/infrastructure/pet-project-api/types/client.ts +export type QueryParams = Record +``` + +## Ошибка API + +Ошибка API тоже относится к REST-модулю. + +```ts +// src/infrastructure/pet-project-api/errors/pet-project-api.error.ts +export class PetProjectApiError extends Error { + constructor( + public readonly status: number, + message: string, + ) { + super(message) + this.name = 'PetProjectApiError' + } +} +``` + +## Базовый клиент + +`client.ts` содержит только транспортную оболочку и сборку инстанса. Прямой `fetch` живёт здесь, а не в компонентах и не в методах верхних слоёв. + +```ts +// src/infrastructure/pet-project-api/client.ts +import { PetProjectApiError } from './errors/pet-project-api.error' +import type { QueryParams } from './types/client' + +export class PetProjectApiClient { + constructor( + private readonly baseUrl: string, + private readonly defaultHeaders: Record = {}, + ) {} + + async get(path: string, params: QueryParams = {}): Promise { + const base = `${this.baseUrl.replace(/\/+$/, '')}/` + const url = new URL(path.replace(/^\/+/, ''), base) + + Object.entries(params).forEach(([key, value]) => { + url.searchParams.set(key, String(value)) + }) + + const response = await fetch(url, { + headers: { + Accept: 'application/json', + ...this.defaultHeaders, + }, + }) + + if (!response.ok) { + throw new PetProjectApiError(response.status, response.statusText) + } + + return response.json() as Promise + } +} +``` + +Это минимальный шаблон. Авторизация, дополнительные заголовки, `next.revalidate`, `post`, `formdata` и другие детали добавляются только когда они реально нужны API. + +## Методы API + +Методы группируются по сущностям в `methods/`. Они не знают про React, SWR и UI. + +```ts +// src/infrastructure/pet-project-api/methods/posts.ts +import type { PetProjectApiClient } from '../client' +import type { PostDto, PostListQueryDto } from '../types/post' + +export function postsMethods(client: PetProjectApiClient) { + return { + /** GET /posts */ + list: (query: PostListQueryDto = {}) => + client.get('posts', query), + + /** GET /posts/{slug} */ + get: (slug: string) => + client.get(`posts/${slug}`), + } +} +``` + +Метод возвращает DTO в форме API. Если данным нужен доменный смысл, маппинг делается выше, в `business/`. + +## Публичный API + +`index.ts` собирает именованный API-объект и открывает наружу только публичные части модуля. + +```ts +// src/infrastructure/pet-project-api/index.ts +import { PetProjectApiClient } from './client' +import { postsMethods } from './methods/posts' + +const client = new PetProjectApiClient( + process.env.NEXT_PUBLIC_API_URL ?? '', + { 'Content-Type': 'application/json' }, +) + +export const petProjectApi = { + posts: postsMethods(client), +} + +export { PetProjectApiError } from './errors/pet-project-api.error' +export type { PostDto, PostListQueryDto } from './types' +export * from './hooks' +``` + +Внешний код импортирует только из `infrastructure/pet-project-api`, не из внутренних файлов модуля. + +## Правила + +- `fetch` используется только внутри базового клиента. +- DTO запросов и ответов живут в `types/`. +- `client.ts` не содержит DTO, GET-хуки и бизнес-логику. +- Методы лежат в `methods/` и возвращают DTO. +- GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components. +- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`. + +Следующий шаг: [GET-хуки REST-клиента](./hooks.md) или [Стратегии получения данных](../strategies/index.md). diff --git a/ai/nextjs-style-guide/data/rest/index.md b/ai/nextjs-style-guide/data/rest/index.md new file mode 100644 index 0000000..db7df17 --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/index.md @@ -0,0 +1,74 @@ +--- +title: REST +description: Как правильно работать с REST API в проекте. +keywords: [rest, api, данные, infrastructure, клиент, swr, стратегии] +--- + +# REST + +Раздел описывает, как правильно работать с REST API в проекте: создать клиент сервиса и выбрать способ получения данных в приложении. + +REST в проекте проходит через два главных этапа: + +1. Создание клиента. +2. Использование. + +## 1. Создание клиента + +На этом этапе внешний API оформляется как модуль слоя `infrastructure/`. + +Клиент отвечает за: + +- генерацию или ручное описание методов API; +- настройку `baseUrl`; +- заголовки и авторизацию; +- обработку ошибок; +- кастомизацию и расширение типов; +- GET-хуки для клиентских компонентов; +- публичный API модуля. + +Если у API есть OpenAPI-спецификация — клиент генерируется автоматически. Если OpenAPI нет или он неполный — клиент создаётся вручную. + +GET-хуки относятся к клиенту, потому что это прозрачные SWR-обёртки над GET-методами этого клиента. + +Подробнее: + +- [Создание клиента](./clients/index.md) +- [Автогенерация из OpenAPI](./clients/auto.md) +- [Ручное создание](./clients/manual.md) +- [GET-хуки REST-клиента](./clients/hooks.md) + +## 2. Использование + +После создания клиента нужно определить рендер страницы и выбрать, как получать данные в конкретном месте приложения. + +Раздел использования отвечает на вопросы: + +- как понять, можно ли сохранить static/ISR; +- когда страница становится dynamic/SSR; +- когда получать данные через серверный `await`; +- когда запускать несколько серверных запросов параллельно; +- когда передавать промис ниже по дереву; +- когда передавать начальные данные клиентским GET-хукам; +- когда использовать GET-хук в клиентском компоненте; +- когда выносить композицию и бизнес-смысл в `business/`. + +Подробнее: + +- [Стратегии получения данных](./strategies/index.md) +- [Серверный await](./strategies/server-await.md) +- [Параллельные серверные запросы](./strategies/parallel-server-requests.md) +- [Передача промиса ниже](./strategies/pass-promise-down.md) +- [Начальные данные для клиентских хуков](./strategies/client-hooks-initial-data.md) +- [Клиентский GET-хук](./strategies/client-get-hook.md) +- [Business-композиция](./strategies/business-composition.md) + +## Как читать раздел + +Если API ещё не подключён — начните с [Создания клиента](./clients/index.md). + +Если клиент уже есть, но непонятно как получить данные — начните со [Стратегий получения данных](./strategies/index.md). + +Если данные нужны в Client Component — сначала проверьте, есть ли [GET-хук REST-клиента](./clients/hooks.md). + +Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` — это уже не REST-клиент, а `business/`. diff --git a/ai/nextjs-style-guide/data/rest/strategies/business-composition.md b/ai/nextjs-style-guide/data/rest/strategies/business-composition.md new file mode 100644 index 0000000..5234443 --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/strategies/business-composition.md @@ -0,0 +1,121 @@ +--- +title: Business-композиция +description: Когда REST-данные нужно объединить или интерпретировать в бизнес-модуле. +keywords: [rest, business, композиция, hooks, domain, isAuth] +--- + +# Business-композиция + +Business-композиция используется, когда простого GET-метода или прозрачного GET-хука недостаточно: нужно объединить несколько источников, преобразовать DTO или вычислить доменное состояние. + +## Когда использовать + +- Нужно объединить несколько GET-запросов. +- Нужно вычислить `isAuth`, `canEdit`, `hasAccess`, `hasPets`. +- Нужно преобразовать DTO в доменную модель. +- Нужно спрятать бизнес-сценарий за доменным API. + +Такая логика не пишется в `infrastructure/`. REST-клиент остаётся прозрачным адаптером к API. + +## Пример поверх одного GET-хука + +```ts +// src/business/pets/hooks/use-available-pets.hook.ts +import { useGetPetList } from 'infrastructure/pet-store-api' + +/** + * Доменный список доступных питомцев. + */ +export const useAvailablePets = () => { + const query = useGetPetList('available') + + return { + ...query, + hasPets: Boolean(query.data?.length), + } +} +``` + +`useGetPetList` — infrastructure-хук. `hasPets` — бизнес-интерпретация, поэтому она появляется в `business/pets`. + +## Пример композиции нескольких GET-хуков + +```ts +// src/business/pets/hooks/use-pets-dashboard.hook.ts +import { useGetPetList } from 'infrastructure/pet-store-api' + +/** + * Данные dashboard по питомцам. + */ +export const usePetsDashboard = () => { + const availablePets = useGetPetList('available') + const pendingPets = useGetPetList('pending') + const soldPets = useGetPetList('sold') + + return { + availablePets, + pendingPets, + soldPets, + total: + (availablePets.data?.length ?? 0) + + (pendingPets.data?.length ?? 0) + + (soldPets.data?.length ?? 0), + } +} +``` + +Композиция нескольких запросов не добавляется в `infrastructure/pet-store-api/hooks/`, потому что это уже сценарий потребления данных. + +## Пример auth-состояния + +```ts +// src/business/auth/hooks/use-auth-state.hook.ts +import { useGetCurrentUser } from 'infrastructure/backend-api' + +/** + * Состояние авторизации текущего пользователя. + */ +export const useAuthState = () => { + const currentUser = useGetCurrentUser() + const user = currentUser.data + + return { + ...currentUser, + user, + isAuth: Boolean(user), + } +} +``` + +`isAuth` не является частью REST-клиента. Это доменный смысл результата запроса. + +## Где размещать + +```text +src/business/ +└── pets/ + ├── hooks/ + │ └── use-available-pets.hook.ts + ├── mappers/ + │ └── map-pet-dto-to-pet.ts + ├── types/ + └── index.ts +``` + +Модуль `business/` экспортирует наружу готовый доменный API через `index.ts`. + +## Что запрещено + +```ts +// Плохо — business-смысл внутри infrastructure-хука +export const useGetPetList = (status: PetStatus) => { + const query = useSWR(...) + + return { + ...query, + hasPets: Boolean(query.data?.length), + } +} +``` + +REST-модуль отвечает за доступ к API. Business-модуль отвечает за смысл этих данных в продукте. diff --git a/ai/nextjs-style-guide/data/rest/strategies/client-get-hook.md b/ai/nextjs-style-guide/data/rest/strategies/client-get-hook.md new file mode 100644 index 0000000..be3265c --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/strategies/client-get-hook.md @@ -0,0 +1,89 @@ +--- +title: Клиентский GET-хук +description: Получение REST-данных в Client Components через готовые GET-хуки REST-клиента. +keywords: [rest, client components, swr, get-хук, client state] +--- + +# Клиентский GET-хук + +Клиентский GET-хук используется, когда данные зависят от состояния браузера: вкладки, фильтра, поиска, пагинации, модалки или действия пользователя. + +## Когда использовать + +- Запрос зависит от client state. +- Данные не обязательны для первого HTML. +- Пользователь меняет параметры запроса на клиенте. +- Нужны SWR-кеширование, дедупликация и ревалидация. + +## Пример с вкладками + +```tsx +'use client' + +import { useState } from 'react' +import { useGetPetList } from 'infrastructure/pet-store-api' +import type { PetStatus } from 'infrastructure/pet-store-api' + +const statuses: PetStatus[] = ['available', 'pending', 'sold'] + +export function PetTabs() { + const [status, setStatus] = useState('available') + const { data: pets, isLoading, error } = useGetPetList(status) + + return ( +
+
+ {statuses.map((item) => ( + + ))} +
+ + {isLoading &&
Загрузка...
} + {error &&
Ошибка загрузки
} + +
    + {pets?.map((pet) => ( +
  • {pet.name}
  • + ))} +
+
+ ) +} +``` + +Компонент выбирает параметр `status`, но не знает про SWR-ключ и fetcher. Запрос выполняет готовый GET-хук REST-клиента. + +## Если хука нет + +Хук добавляется в REST-модуль сервиса: + +```text +src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts +``` + +Не создавайте локальный `useSWR` в компоненте. + +## Плохо + +```tsx +// Плохо — прямой вызов клиента в useEffect +useEffect(() => { + petStoreApi.pet.findPetsByStatus({ status }).then(setPets) +}, [status]) + +// Плохо — useSWR в компоненте +const { data } = useSWR( + ['pet-store-api', 'pet', 'list', status], + () => petStoreApi.pet.findPetsByStatus({ status }), +) +``` + +Такой код теряет единое место для ключей, дублирует fetcher и разносит инфраструктурные детали по UI. + +## Когда выбрать другую стратегию + +- Данные нужны до первого HTML — [Серверный await](./server-await.md). +- Клиентский хук должен получить начальные данные сразу — [Начальные данные для клиентских хуков](./client-hooks-initial-data.md). +- Нужно вычислить бизнес-состояние — [Business-композиция](./business-composition.md). diff --git a/ai/nextjs-style-guide/data/rest/strategies/client-hooks-initial-data.md b/ai/nextjs-style-guide/data/rest/strategies/client-hooks-initial-data.md new file mode 100644 index 0000000..1f22fa5 --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/strategies/client-hooks-initial-data.md @@ -0,0 +1,109 @@ +--- +title: Начальные данные для клиентских хуков +description: Как дать клиентским GET-хукам начальные REST-данные. +keywords: [rest, swr, fallback, initial data, client hooks, unstable_serialize, isr, ssr] +--- + +# Начальные данные для клиентских хуков + +Как дать клиентским GET-хукам начальные REST-данные. + +Эта стратегия используется, когда данные должны быть запущены на сервере, но потребляться на клиенте через GET-хуки REST-клиента. + +Технически это делается через `SWRConfig fallback`: сервер передаёт промис в fallback, а клиентский хук использует тот же SWR-ключ. + +## Когда использовать + +- Внутри страницы есть Client Components с GET-хуками. +- Нужно начать загрузку данных на сервере раньше. +- Клиентский компонент должен остаться обычным потребителем `useGetPetList(...)`. +- Не нужно писать отдельный prop-drilling для начальных данных. + +## Рендер страницы + +Перед этой стратегией сначала определите рендер маршрута. Серверный preload для `fallback` подчиняется тем же правилам, что и любой серверный запрос в `page.tsx` или `layout.tsx`. + +Если данные общие и могут обновляться по интервалу, сохраняйте static/ISR. Если preload зависит от cookie, headers, `searchParams`, `no-store` или персональных данных пользователя, маршрут становится dynamic/SSR. + +`SWRConfig fallback` не должен быть причиной отключать ISR на всякий случай. Он только передаёт клиентскому GET-хуку данные, которые уже были запущены на сервере. + +## Ключ хука + +```ts +// src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts +export const getPetListKey = (status: PetStatus) => + ['pet-store-api', 'pet', 'list', status] as const +``` + +Ключ экспортируется из REST-модуля, потому что он нужен и GET-хуку, и серверному `SWRConfig fallback`. + +## Пример layout + +```tsx +// src/app/(routes)/pets/layout.tsx +import type { ReactNode } from 'react' +import { SWRConfig, unstable_serialize } from 'swr' +import { + getPetListKey, + petStoreApi, +} from 'infrastructure/pet-store-api' + +type PetsLayoutProps = { + children: ReactNode +} + +export default async function PetsLayout({ children }: PetsLayoutProps) { + const availablePetsPromise = petStoreApi.pet.findPetsByStatus({ + status: 'available', + }) + + return ( + + {children} + + ) +} +``` + +Если GET-хук использует array-key, ключ для `fallback` сериализуется через `unstable_serialize`. + +## Клиентский компонент + +```tsx +'use client' + +import { useGetPetList } from 'infrastructure/pet-store-api' + +export function PetList() { + const { data: pets, isLoading } = useGetPetList('available') + + if (isLoading) return
Загрузка...
+ + return ( +
    + {pets?.map((pet) => ( +
  • {pet.name}
  • + ))} +
+ ) +} +``` + +Компонент не знает, что данные были запущены на сервере. Он использует обычный GET-хук REST-клиента. + +## Что важно + +- Ключ `fallback` должен совпадать с ключом GET-хука. +- Серверный код вызывает метод клиента, а не GET-хук. +- Клиентский компонент вызывает GET-хук, а не `useSWR` напрямую. +- Эта стратегия не означает ручную работу с кешем в компонентах. + +## Когда не использовать + +Если данные нужны только серверному компоненту, используйте [Серверный await](./server-await.md). Если данные зависят от состояния браузера, используйте [Клиентский GET-хук](./client-get-hook.md). diff --git a/ai/nextjs-style-guide/data/rest/strategies/index.md b/ai/nextjs-style-guide/data/rest/strategies/index.md new file mode 100644 index 0000000..a7e6d34 --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/strategies/index.md @@ -0,0 +1,100 @@ +--- +title: Стратегии получения данных +description: Как выбрать получение REST-данных с учётом рендера страницы. +keywords: [rest, стратегии, render, isr, ssr, server components, swr, promise, business] +--- + +# Стратегии получения данных + +Как выбрать получение REST-данных с учётом рендера страницы. + +Перед выбором стратегии должен быть создан REST-клиент сервиса. Если клиента ещё нет, начните с раздела [Создание клиента](../clients/index.md). + +## Сначала определите рендер страницы + +В Next.js выбор начинается не с `await`, `Suspense` или SWR. Сначала нужно понять, какой рендер получится у маршрута: static/ISR или dynamic/SSR. + +Next.js может перевести страницу в dynamic rendering автоматически, если в маршруте используются API текущего запроса. Поэтому первый вопрос такой: + +```text +Можно ли сохранить ISR, или странице нужны данные на каждый request? +``` + +ISR — приоритет. Если данные общие для пользователей и их можно обновлять с интервалом, не переводите страницу в SSR без необходимости. + +SSR/dynamic rendering выбирается только когда данные действительно зависят от текущего request или должны пересчитываться на каждый запрос. + +## Что переводит страницу в dynamic rendering + +Проверьте, нужны ли странице API и настройки, которые делают маршрут динамическим: + +- `cookies()` — данные зависят от cookie текущего пользователя. +- `headers()` — данные зависят от request headers. +- `draftMode()` — нужен preview/draft-режим. +- `searchParams` в `page.tsx` — данные зависят от query string. +- `cache: 'no-store'` или `revalidate: 0` в методе клиента — запрос нельзя кешировать. +- `connection()` — рендер явно ждёт request. +- `export const dynamic = 'force-dynamic'` — SSR включён вручную. + +Если ничего из этого не нужно, сначала проектируйте страницу как static/ISR. Серверный `await` сам по себе не означает SSR: режим зависит от кеширования запроса и dynamic API маршрута. + +## Рендер перед стратегией + +| Рендер | Когда подходит | Что выбирать дальше | +|--------|----------------|---------------------| +| Static/ISR | Данные общие и могут обновляться по интервалу | Серверные стратегии: `await`, `Promise.all`, передача промиса ниже, SWR `fallback` | +| SSR/dynamic | Данные зависят от request, пользователя или должны быть свежими на каждый запрос | Серверные стратегии с учётом блокировки первого HTML | +| После гидрации | Данные зависят от вкладки, фильтра, поиска, пагинации или действия пользователя | Клиентский GET-хук | + +## Как выбрать стратегию + +Когда режим рендера понятен, выбирайте конкретный способ получения данных: + +| Ситуация после выбора рендера | Стратегия | Где читать | +|-------------------------------|-----------|------------| +| Данные обязательны для первого HTML, SEO, `notFound()` или `redirect()` | Серверный `await` | [Серверный await](./server-await.md) | +| Несколько независимых данных нужны до рендера | Запуск промисов + `Promise.all` | [Параллельные серверные запросы](./parallel-server-requests.md) | +| Часть UI можно загрузить отдельно | Передача промиса ниже + `Suspense` | [Передача промиса ниже](./pass-promise-down.md) | +| Client Component должен получить данные сразу из SWR | Начальные данные для клиентских хуков | [Начальные данные для клиентских хуков](./client-hooks-initial-data.md) | +| Данные зависят от client state | Клиентский GET-хук | [Клиентский GET-хук](./client-get-hook.md) | +| Нужно объединить несколько запросов или вычислить `isAuth`, `canEdit`, `hasPets` | Business-композиция | [Business-композиция](./business-composition.md) | + +## Правило выбора + +Не выбирайте стратегию по любимому инструменту. Выбирайте её по двум вопросам: + +```text +Можно ли сохранить ISR? +Где нужны данные и что должно произойти до первого HTML? +``` + +Если данные можно кешировать между пользователями — сохраняйте static/ISR. Если данные request-specific — используйте SSR/dynamic rendering. Если данные зависят от состояния браузера — используйте GET-хук REST-клиента. Если простой GET превращается в доменный сценарий — переходите в `business/`. + +## Общие запреты + +```tsx +// Плохо — SSR включён на всякий случай +export const dynamic = 'force-dynamic' + +// Плохо — ISR отключён без требования к свежести на каждый request +export const revalidate = 0 + +// Плохо — прямой fetch в компоненте +useEffect(() => { + fetch('/api/pets').then(...) +}, []) + +// Плохо — useSWR в компоненте +const { data } = useSWR( + ['pet-store-api', 'pet', 'list', status], + () => petStoreApi.pet.findPetsByStatus({ status }), +) + +// Плохо — бизнес-флаг внутри GET-хука REST-клиента +return { + ...query, + hasPets: Boolean(query.data?.length), +} +``` + +Не отключайте ISR без причины. В компонентах используются готовые методы клиента или готовые хуки. SWR-ключи, fetcher и транспорт остаются внутри REST-модуля. diff --git a/ai/nextjs-style-guide/data/rest/strategies/parallel-server-requests.md b/ai/nextjs-style-guide/data/rest/strategies/parallel-server-requests.md new file mode 100644 index 0000000..c7c0e7f --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/strategies/parallel-server-requests.md @@ -0,0 +1,82 @@ +--- +title: Параллельные серверные запросы +description: Как запускать независимые REST-запросы на сервере без waterfall. +keywords: [rest, promise.all, параллельные запросы, server components] +--- + +# Параллельные серверные запросы + +Если серверному компоненту нужно несколько независимых данных, запускайте запросы до ожидания результата. Последовательный `await` создаёт waterfall и замедляет рендер. + +## Когда использовать + +- Запросы независимы друг от друга. +- Все данные нужны текущему серверному компоненту перед возвратом UI. +- Нельзя или не нужно стримить часть UI отдельно. + +## Хорошо + +```tsx +import { petStoreApi } from 'infrastructure/pet-store-api' +import { PetsDashboardScreen } from 'screens/pets-dashboard' + +export default async function PetsDashboardPage() { + const availablePetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' }) + const pendingPetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'pending' }) + const soldPetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'sold' }) + + const [availablePets, pendingPets, soldPets] = await Promise.all([ + availablePetsPromise, + pendingPetsPromise, + soldPetsPromise, + ]) + + return ( + + ) +} +``` + +## Плохо + +```tsx +export default async function PetsDashboardPage() { + const availablePets = await petStoreApi.pet.findPetsByStatus({ status: 'available' }) + const pendingPets = await petStoreApi.pet.findPetsByStatus({ status: 'pending' }) + const soldPets = await petStoreApi.pet.findPetsByStatus({ status: 'sold' }) + + return ( + + ) +} +``` + +Во втором примере каждый следующий запрос ждёт предыдущий, хотя они независимы. + +## Зависимые запросы + +Если второй запрос зависит от результата первого, последовательный `await` допустим: + +```tsx +export default async function OrderPage({ params }: OrderPageProps) { + const { id } = await params + const order = await petStoreApi.store.getOrderById(Number(id)) + const pet = await petStoreApi.pet.getPetById(order.petId) + + return +} +``` + +Не превращайте зависимый сценарий в `Promise.all` искусственно. + +## Когда выбрать другую стратегию + +Если часть данных не обязательна для первого блока UI, можно запустить промис выше и передать его ниже: [Передача промиса ниже](./pass-promise-down.md). diff --git a/ai/nextjs-style-guide/data/rest/strategies/pass-promise-down.md b/ai/nextjs-style-guide/data/rest/strategies/pass-promise-down.md new file mode 100644 index 0000000..56fa7e3 --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/strategies/pass-promise-down.md @@ -0,0 +1,62 @@ +--- +title: Передача промиса ниже +description: Как запускать серверный REST-запрос выше и ожидать его во вложенном server-компоненте. +keywords: [rest, promise, suspense, streaming, server components] +--- + +# Передача промиса ниже + +Серверный компонент может запустить запрос и передать промис вложенному server-компоненту. Это полезно, когда часть UI можно загрузить отдельно через `Suspense`. + +## Когда использовать + +- Верхняя часть страницы может отрендериться без этих данных. +- Данные нужны только вложенному server-компоненту. +- Нужна `Suspense`-граница и серверный стриминг. + +## Пример + +```tsx +// src/app/(routes)/pets/page.tsx +import { Suspense } from 'react' +import { petStoreApi } from 'infrastructure/pet-store-api' +import { PetListSection } from 'widgets/pet-list-section' +import { PetListSkeleton } from 'widgets/pet-list-section' +import type { Pet } from 'infrastructure/pet-store-api' + +export default function PetsPage() { + const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' }) + + return ( +
+

Питомцы

+ }> + + +
+ ) +} + +async function AvailablePets({ petsPromise }: { petsPromise: Promise }) { + const pets = await petsPromise + + return +} +``` + +Запрос стартует в `PetsPage`, но ожидание происходит внутри `AvailablePets`. `Suspense` управляет fallback для этой части UI. + +## Граница стратегии + +Эта стратегия остаётся серверной. Не используйте её как замену GET-хукам в Client Components. + +Если данные должны попасть в клиентский SWR-хук, используйте [Начальные данные для клиентских хуков](./client-hooks-initial-data.md). + +## Что не делать + +```tsx +// Плохо — передавать промис в произвольный клиентский компонент без ясной стратегии +return +``` + +Для клиентского потребления есть отдельная стратегия через `SWRConfig fallback` и готовые GET-хуки REST-клиента. diff --git a/ai/nextjs-style-guide/data/rest/strategies/server-await.md b/ai/nextjs-style-guide/data/rest/strategies/server-await.md new file mode 100644 index 0000000..a882857 --- /dev/null +++ b/ai/nextjs-style-guide/data/rest/strategies/server-await.md @@ -0,0 +1,88 @@ +--- +title: Серверный await +description: Получение REST-данных на сервере до первого HTML. +keywords: [rest, server components, await, nextjs, isr, ssr, notFound, redirect] +--- + +# Серверный await + +Получение REST-данных на сервере до первого HTML. + +Серверный `await` — базовая стратегия для данных, которые нужны до рендера страницы или серверного блока. + +## Когда использовать + +- Данные нужны для первого HTML. +- Данные влияют на `metadata`. +- По результату запроса нужно вызвать `notFound()` или `redirect()`. +- Компонент серверный и данные не зависят от состояния браузера. + +## Влияние на рендер + +Серверный `await` сам по себе не означает SSR. В App Router страница может остаться static/ISR, если маршрут не использует dynamic API и запросы можно кешировать. + +ISR — приоритет для общих данных. Если список или детальная страница могут обновляться по интервалу, сохраняйте кеширование и не добавляйте `no-store`, `revalidate: 0` или `force-dynamic` без требования. + +SSR/dynamic rendering нужен, когда данные зависят от текущего request: cookie, headers, `searchParams`, preview-режим или персональные данные пользователя. + +## Пример страницы списка + +```tsx +// src/app/(routes)/pets/page.tsx +import { petStoreApi } from 'infrastructure/pet-store-api' +import { PetsScreen } from 'screens/pets' + +export default async function PetsPage() { + const pets = await petStoreApi.pet.findPetsByStatus({ + status: 'available', + }) + + return +} +``` + +`page.tsx` получает данные первого рендера и передаёт их ниже. UI страницы остаётся в `screens/`, а не пишется прямо в `app/`. + +## Пример детальной страницы + +```tsx +// src/app/(routes)/pets/[id]/page.tsx +import { notFound } from 'next/navigation' +import { petStoreApi } from 'infrastructure/pet-store-api' +import { PetDetailScreen } from 'screens/pet-detail' + +type PetPageProps = { + params: Promise<{ id: string }> +} + +export default async function PetPage({ params }: PetPageProps) { + const { id } = await params + const pet = await petStoreApi.pet.getPetById(Number(id)).catch(() => null) + + if (!pet) { + notFound() + } + + return +} +``` + +Обработка 404 зависит от API-клиента и класса ошибок. В примере показана идея: решение о `notFound()` принимается на уровне маршрута, а не внутри REST-клиента. + +## Что не делать + +```tsx +// Плохо — хуки нельзя вызывать в Server Component +const { data } = useGetPetList('available') + +// Плохо — прямой fetch в обход клиента +const response = await fetch('https://petstore3.swagger.io/api/v3/pet/findByStatus') +``` + +Если данные нужны на сервере, вызывайте метод REST-клиента напрямую. + +## Когда выбрать другую стратегию + +- Несколько независимых запросов — [Параллельные серверные запросы](./parallel-server-requests.md). +- Часть UI можно грузить отдельно — [Передача промиса ниже](./pass-promise-down.md). +- Данные нужны клиентскому хуку сразу после гидрации — [Начальные данные для клиентских хуков](./client-hooks-initial-data.md). diff --git a/ai/nextjs-style-guide/workflow.md b/ai/nextjs-style-guide/workflow.md new file mode 100644 index 0000000..c060231 --- /dev/null +++ b/ai/nextjs-style-guide/workflow.md @@ -0,0 +1,8 @@ +--- +title: Подсказки +description: Короткие ответы на типовые вопросы и решения для спорных ситуаций. +--- + +# Подсказки + +Короткие ответы на типовые вопросы и решения для спорных ситуаций. diff --git a/public/img/sprites/icons/sprite.stack.html b/public/img/sprites/icons/sprite.stack.html deleted file mode 100644 index df088b3..0000000 --- a/public/img/sprites/icons/sprite.stack.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - SVG stack preview | svg-sprite - - - - - - - - - - -
-

SVG stack preview

-

This preview features an SVG stack. Please have a look at the HTML source for further details and be aware of the following constraints:

- -
-
- - - -
    - -
  • -
    - arrow-down -
    -

    arrow-down,

    -
  • -
  • -
    - arrow-right -
    -

    arrow-right,

    -
  • -
- - - -
-
-

Generated at Tue, 21 Apr 2026 18:16:57 GMT by svg-sprite.

-
- - diff --git a/public/img/sprites/icons/sprite.stack.svg b/public/img/sprites/icons/sprite.stack.svg deleted file mode 100644 index 57f399b..0000000 --- a/public/img/sprites/icons/sprite.stack.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/scripts/create-svg-sprite.js b/scripts/create-svg-sprite.js deleted file mode 100644 index 71f97d4..0000000 --- a/scripts/create-svg-sprite.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Генерация SVG-спрайтов и TypeScript-типов имён иконок. - * - * Читает подпапки из ASSETS_DIR, для каждой собирает SVG в спрайт (stack или symbol) - * и генерирует .generated.ts файл с union-типом имён иконок. - * - * Режим спрайта определяется суффиксом папки: «icons?symbol» → symbol, иначе stack. - * - * Запуск: npm run sprite - */ -const fs = require('fs') -const path = require('path') -const SVGSpriter = require('svg-sprite') -const color = require('colorette') - -const ROOT = process.cwd() - -/** Папка с исходными SVG-файлами. */ -const ASSETS_DIR = path.join(ROOT, 'src/shared/sprites') - -/** Папка для сгенерированных спрайтов. */ -const DEST_DIR = path.join(ROOT, 'public/img/sprites') - -/** - * Преобразует kebab-case строку в PascalCase. - */ -const toPascalCase = (str) => - str.replace(/(^|[-_])([a-z])/g, (_, __, c) => c.toUpperCase()) - -/** - * Возвращает конфигурацию режима для svg-sprite. - */ -const getModeConfig = (mode, destDir) => ({ - dest: destDir, - sprite: `sprite.${mode}.svg`, - example: true, - rootviewbox: false, -}) - -/** - * Генерирует TypeScript-файл с union-типом имён иконок спрайта. - */ -const generateIconNames = (folderName, svgFiles) => { - const names = svgFiles - .map((filePath) => path.basename(filePath, '.svg')) - .sort() - - const typeName = `${toPascalCase(folderName)}IconName` - - const content = [ - '/**', - ` * Имена иконок спрайта «${folderName}».`, - ' * @generated — файл создан автоматически (npm run sprite), не редактировать вручную.', - ' */', - `export type ${typeName} =`, - names.map((name) => ` | '${name}'`).join('\n'), - '', - ].join('\n') - - const outputPath = path.join(ASSETS_DIR, `${folderName}.generated.ts`) - fs.writeFileSync(outputPath, content) - console.log( - color.green(`Generated types: ${folderName}.generated.ts (${names.length} icons)`), - ) -} - -/** - * Обрабатывает одну папку со спрайтами. - */ -const processFolder = (fullFolderName) => { - const folderPath = path.join(ASSETS_DIR, fullFolderName) - - if (!fs.lstatSync(folderPath).isDirectory()) { - return - } - - const hasCustomMode = fullFolderName.includes('?') - const parts = fullFolderName.split('?') - const mode = hasCustomMode ? parts.pop() : 'stack' - const folderName = parts[0] - - const svgFiles = fs - .readdirSync(folderPath) - .filter((file) => file.endsWith('.svg')) - .map((file) => path.join(folderPath, file)) - - if (!svgFiles.length) { - return - } - - const config = { - log: 'debug', - mode: { - [mode]: getModeConfig(mode, path.join(DEST_DIR, folderName)), - }, - } - - const spriter = new SVGSpriter(config) - - for (const fileName of svgFiles) { - spriter.add(fileName, null, fs.readFileSync(fileName, 'utf-8')) - } - - spriter.compile((error, result) => { - if (error) { - console.log(color.red(error.message)) - return - } - - for (const modeResult of Object.values(result)) { - for (const resource of Object.values(modeResult)) { - fs.mkdirSync(path.dirname(resource.path), { recursive: true }) - fs.writeFileSync(resource.path, resource.contents) - } - } - }) - - generateIconNames(folderName, svgFiles) -} - -try { - const entries = fs.readdirSync(ASSETS_DIR) - entries.forEach(processFolder) -} catch (err) { - console.log(color.red(err.message)) -} diff --git a/src/app/providers/index.ts b/src/app/providers/index.ts deleted file mode 100644 index c248e7a..0000000 --- a/src/app/providers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { MantineProvider } from './mantine-provider'; diff --git a/src/app/providers/mantine-provider.tsx b/src/app/providers/mantine-provider.tsx deleted file mode 100644 index bab72f2..0000000 --- a/src/app/providers/mantine-provider.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'use client'; - -import { MantineProvider as BaseMantineProvider } from '@mantine/core'; -import type { PropsWithChildren } from 'react'; - -/** - * Провайдер Mantine UI. - */ -export const MantineProvider = ({ children }: PropsWithChildren) => { - return {children}; -}; diff --git a/src/app/styles/index.css b/src/app/styles/index.css deleted file mode 100644 index 8671c66..0000000 --- a/src/app/styles/index.css +++ /dev/null @@ -1,3 +0,0 @@ -@import "./variables.css"; -@import "./media.css"; -@import "./reset.css"; diff --git a/src/app/styles/media.css b/src/app/styles/media.css deleted file mode 100644 index 74c04b0..0000000 --- a/src/app/styles/media.css +++ /dev/null @@ -1,6 +0,0 @@ -/* Медиа-запросы (Mobile First) */ -@custom-media --xs (min-width: 36em); -@custom-media --sm (min-width: 48em); -@custom-media --md (min-width: 62em); -@custom-media --lg (min-width: 75em); -@custom-media --xl (min-width: 88em); diff --git a/src/app/styles/reset.css b/src/app/styles/reset.css deleted file mode 100644 index 69b15aa..0000000 --- a/src/app/styles/reset.css +++ /dev/null @@ -1,28 +0,0 @@ -/* Базовые стили */ -html { - height: 100%; -} - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - -body { - min-height: 100%; - display: flex; - flex-direction: column; - color: var(--color-text); - background: var(--color-bg); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} diff --git a/src/app/styles/variables.css b/src/app/styles/variables.css deleted file mode 100644 index eae5064..0000000 --- a/src/app/styles/variables.css +++ /dev/null @@ -1,35 +0,0 @@ -/* Цвета */ -:root { - --color-text: #1a1a1a; - --color-text-secondary: #6b7280; - --color-bg: #ffffff; - --color-bg-secondary: #f9fafb; - --color-border: #e5e7eb; - --color-primary: #228be6; - --color-error: #fa5252; - --color-success: #40c057; - --color-warning: #fab005; -} - -/* Отступы */ -:root { - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 20px; - --space-6: 24px; - --space-8: 32px; - --space-10: 40px; - --space-12: 48px; - --space-16: 64px; -} - -/* Скругления */ -:root { - --radius-1: 4px; - --radius-2: 8px; - --radius-3: 12px; - --radius-4: 16px; - --radius-full: 9999px; -}