feat: добавить хаб документаций

- добавлен React/Vite-лендинг с карточками документаций
- добавлена генерация корневого llms.txt из конфига документов
- добавлена сборка SLM Design через VitePress
- добавлены Dockerfile, Caddyfile и Gitea CI/CD
- настроены контекстные Link headers для llms.txt
This commit is contained in:
2026-05-13 10:12:31 +03:00
commit 86ab6bc8fd
104 changed files with 44009 additions and 0 deletions

12
.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
node_modules
.git
.gitea
.claude
dist
docs/*/.vitepress/cache
docs/*/.vitepress/dist
docs/*/content
public/slm-design
*.log
.DS_Store
.env*

126
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,126 @@
name: CI/CD Pipeline
on:
push:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Установка зависимостей
run: npm ci
- name: Сборка документации SLM Design
run: npm run docs:build:slm-design
- name: Генерация корневых артефактов
run: npm run site:generate
- name: Сборка лендинга
run: npm run build
docker:
runs-on: ubuntu-latest
needs: build
if: "github.ref == 'refs/heads/master' && !contains(github.event.head_commit.message, '[skip ci]')"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: master
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Setup variables
run: |
DOCKER_REGISTRY=$(echo "${{ gitea.server_url }}" | sed 's|https://||')
echo "DOCKER_REGISTRY=$DOCKER_REGISTRY" >> $GITHUB_ENV
REGISTRY_IMAGE="$DOCKER_REGISTRY/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')"
echo "REGISTRY_IMAGE=$REGISTRY_IMAGE" >> $GITHUB_ENV
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.CR_USER }}
password: ${{ secrets.CR_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=ref,event=branch
type=sha,prefix=
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: false
sbom: false
deploy:
runs-on: ubuntu-latest
needs: docker
if: "github.ref == 'refs/heads/master' && !contains(github.event.head_commit.message, '[skip ci]')"
steps:
- name: Setup variables
run: |
DOCKER_REGISTRY=$(echo "${{ gitea.server_url }}" | sed 's|https://||')
echo "DOCKER_REGISTRY=$DOCKER_REGISTRY" >> $GITHUB_ENV
REGISTRY_IMAGE="$DOCKER_REGISTRY/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')"
echo "REGISTRY_IMAGE=$REGISTRY_IMAGE" >> $GITHUB_ENV
- name: Настройка SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H 188.225.47.78 >> ~/.ssh/known_hosts
- name: Деплой
run: |
ssh -i ~/.ssh/deploy_key root@188.225.47.78 bash -s <<'SCRIPT'
set -e
IMAGE="${{ env.REGISTRY_IMAGE }}:latest"
CONTAINER="docs"
echo '${{ secrets.CR_TOKEN }}' | docker login ${{ env.DOCKER_REGISTRY }} -u '${{ secrets.CR_USER }}' --password-stdin
OLD_IMAGE_ID=$(docker images -q "$IMAGE" 2>/dev/null || true)
docker pull "$IMAGE"
docker stop "$CONTAINER" 2>/dev/null || true
docker rm "$CONTAINER" 2>/dev/null || true
docker run -d --name "$CONTAINER" --network web --restart unless-stopped "$IMAGE"
NEW_IMAGE_ID=$(docker images -q "$IMAGE")
if [ -n "$OLD_IMAGE_ID" ] && [ "$OLD_IMAGE_ID" != "$NEW_IMAGE_ID" ]; then
docker rmi "$OLD_IMAGE_ID" 2>/dev/null || true
fi
docker image prune -af --filter "label=org.opencontainers.image.title=$CONTAINER"
docker image prune -f
docker builder prune -f 2>/dev/null || true
docker ps --filter "name=$CONTAINER"
SCRIPT

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Generated docs
docs/*/content/
public/llms.txt
public/slm-design/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

49
Caddyfile Normal file
View File

@@ -0,0 +1,49 @@
:8082 {
root * /srv
# Чистые URL: запросы вида `/foo.html` редиректим на `/foo`.
@legacyHtml {
path_regexp legacyHtml ^(/.+)\.html$
not path /index.html
}
redir @legacyHtml {re.legacyHtml.1} 301
# LLM-артефакты остаются в своих пространствах документаций, без редиректа в корень.
@slmDesign path /slm-design /slm-design/*
header @slmDesign Link "</slm-design/llms.txt>; rel=\"llms\""
@nextjsStyleGuide path /nextjs-style-guide /nextjs-style-guide/*
header @nextjsStyleGuide Link "</nextjs-style-guide/llms.txt>; rel=\"llms\""
@reactStyleGuide path /react-style-guide /react-style-guide/*
header @reactStyleGuide Link "</react-style-guide/llms.txt>; rel=\"llms\""
@figmaAdaptiveStandards path /figma-adaptive-standards /figma-adaptive-standards/*
header @figmaAdaptiveStandards Link "</figma-adaptive-standards/llms.txt>; rel=\"llms\""
@root {
not path /slm-design /slm-design/* /nextjs-style-guide /nextjs-style-guide/* /react-style-guide /react-style-guide/* /figma-adaptive-standards /figma-adaptive-standards/*
}
header @root Link "</llms.txt>; rel=\"llms\""
@existingText {
path *.txt
file
}
header @existingText Content-Type "text/plain; charset=utf-8"
@existingMarkdown {
path *.md
file
}
header @existingMarkdown Content-Type "text/markdown; charset=utf-8"
@missingText {
path *.txt *.md
not file
}
respond @missingText 404
file_server
try_files {path} {path}.html {path}/index.html /index.html
}

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run docs:build:slm-design && npm run build
FROM caddy:2-alpine
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=build /app/dist /srv

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -0,0 +1,114 @@
---
title: SLM Design
description: Назначение архитектуры, ключевые принципы и карта разделов документации
---
# SLM Design
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
## Разделы спецификации
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
- [Слои](/architecture/layers) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
- [Модули](/architecture/modules) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
- [Сегменты](/architecture/segments) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
- [Монорепозитории](/architecture/monorepo) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business.
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории.
## Преимущества
### Вертикальная организация домена
Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы.
### Dependency Injection без фреймворков
Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий.
### Разделение ответственности без перегрузки слоёв
Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода.
### Горизонтальная инкапсуляция
Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга.
### Колокация по умолчанию
Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями.
### Явное разделение каркаса и контента
Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью.
### Масштабирование через группировку
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции).
### Адаптация к монорепозиториям
SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы.
## Происхождение
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/
├── infra/
│ ├── theme/
│ ├── i18n/
│ ├── backend-api/
│ └── logger/
├── ui/
│ ├── button/
│ ├── input/
│ ├── modal/
│ ├── toast/
│ └── dropdown/
└── shared/
├── lib/
├── types/
└── styles/
```
## Принципы
- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле.
- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости.
- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API.
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.

View File

@@ -0,0 +1,254 @@
---
title: Слои
description: Иерархия слоёв от app до shared, правила зависимостей и зона ответственности каждого слоя
---
# Слои
Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом.
## Определение
**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.**
## Группы слоёв
Слои делятся на три группы:
| Группа | Слои | Описание |
|--------|------|----------|
| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы |
| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит |
| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги |
## Направление зависимостей
Любой импорт между модулями — только через публичный API.
```
app → [ layouts | screens ] → widgets → business → infra → ui → shared
```
- `layouts` и `screens` — параллельные слои, не импортируют друг друга
- Модули одного слоя в группе «Композиция» изолированы друг от друга
- Модули одного слоя `infra` и `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 и сервисами.
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `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/
```
### Требования
- Один модуль = один бизнес-домен
- Циклические зависимости между доменами запрещены
- Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты
- Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую
- Доменные типы (`User`, `Product`) живут здесь, не в `shared/`
## Слой infra
Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль.
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`.
Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
```text
src/infra/
├── theme/
├── i18n/
├── backend-api/
├── maps-api/
├── logger/
├── feature-flags/
└── realtime/
```
### Требования
- Один модуль = один техсервис
- Импортирует `infra/`, `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
Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене.
Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует.
Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь.
```text
src/shared/
├── lib/
├── types/
├── styles/
└── sprites/
```
### Требования
- Не имеет runtime-состояния

View File

@@ -0,0 +1,215 @@
---
title: Модули
description: Структура модуля, типы (UI, бизнес, инфра), публичный API, отличие модуля от компонента
---
# Модули
Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом.
## Определение
**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.**
Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно.
Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность.
Главная граница модуля — не папка, а ответственность.
## Компонент
**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.**
Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля.
> Компонент отображает. Модуль организует.
Компонент не может:
- Импортировать код проекта за пределами родительского модуля.
- Владеть архитектурными зависимостями.
- Содержать любые компоненты.
- Содержать любые модули.
- Делать внешние запросы.
- Самостоятельно получать данные.
- Выбирать источник данных.
- Композировать данные.
- Вызывать сценарные хуки.
- Оркестрировать сценарий.
- Композировать модули.
- Решать, как устроен процесс.
- Содержать бизнес-логику.
- Содержать сценарную логику.
Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль.
```text
auth/
├── ui/
│ └── logout-button/
│ ├── logout-button.tsx
│ ├── styles/
│ │ └── logout-button.module.css
│ ├── types/
│ │ └── logout-button-props.type.ts
│ └── index.ts
└── index.ts
```
## Что считается модулем
Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу.
Примеры модулей:
- `screens/home/` — модуль страницы.
- `widgets/page-heading/` — модуль виджета.
- `business/auth/` — модуль бизнес-домена.
- `infra/theme/` — модуль инфраструктурного сервиса.
- `ui/button/` — модуль UI-kit сущности.
- `screens/home/parts/hero-section/` — вложенный модуль страницы.
Не считаются модулями:
- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты.
- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`.
- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента.
## Типы модулей
Тип модуля определяет обязательный корневой файл и стартовую структуру.
### UI-модуль
Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне:
```text
header/
├── header.tsx
└── index.ts
```
`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу.
### Бизнес-модуль
Бизнес-модуль — модуль, который строится вокруг публичного runtime API.
Бизнес-модуль обязан иметь фабрику в корне:
```text
auth/
├── auth.factory.ts
├── index.ts
└── types/
```
Фабрика возвращает публичный runtime API модуля.
### Инфраструктурный модуль
Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции.
Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса.
```text
theme/
├── index.ts
├── config/
├── hooks/
├── styles/
└── ui/
```
```text
backend-api/
├── backend-api.client.ts
├── config/
├── types/
└── index.ts
```
## Структура
Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности.
```text
{module-name}/
├── {module-name}.factory.ts # фабрика (для business-модулей)
├── {module-name}.tsx # корневой файл модуля (опционален)
├── ui/ # компоненты модуля
├── parts/ # вложенные модули
├── hooks/ # хуки
├── stores/ # сторы состояния
├── services/ # внешние источники данных
├── mappers/ # трансформация данных между форматами
├── types/ # типы
├── styles/ # стили
├── lib/ # утилиты модуля
├── config/ # константы и конфигурация
└── index.ts # публичный API
```
Подробное описание сегментов — в разделе [Сегменты](/architecture/segments).
## Публичный API
Внешний код импортирует модуль только через публичный API.
```ts
// Хорошо
import { customerFactory } from '@/business/customer'
import type { Customer } from '@/business/customer'
```
```ts
// Плохо
import { validateToken } from '@/business/auth/lib/tokens'
```
`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи.
Внутренние сегменты модуля остаются деталями реализации.
Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика.
```ts
// business/customer/index.ts
export { customerFactory } from './customer.factory'
export type { Customer } from './types/customer.type'
export type { CustomerApi } from './types/customer-api.type'
export type { CustomerDeps } from './types/customer-deps.type'
export type { CustomerFactory } from './types/customer-factory.type'
```
## Фабрика
Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля.
Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика.
Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type``import type` не является runtime-зависимостью.
Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция».
### Примеры
Пример реализации фабрики в React см. в [Создание фабрики](/examples/react/factory).
Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](/examples/react/factory-composition).
Пример композиции фабрик через React Provider см. в [Композиция через Provider](/examples/react/composition-provider).
## Жизненный цикл
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
- Нужен на одной странице → `screens/{name}/parts/`
- Появился в 2+ местах → поднимается по природе:
- абстрактный UI → `ui/`
- блок с данными/логикой → `widgets/`
- представление бизнес-домена → `business/{area}/parts/`
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.

View File

@@ -0,0 +1,235 @@
---
title: Монорепозитории
description: Правила применения SLM Design для frontend-проектов, находящихся в монорепозитории
---
# Монорепозитории
Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов.
## Определение
**Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.**
## Базовая структура
Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`.
```text
repo/
├── apps/
│ ├── web/
│ │ └── src/
│ │ ├── app/
│ │ ├── layouts/
│ │ ├── screens/
│ │ ├── widgets/
│ │ ├── business/
│ │ ├── infra/
│ │ ├── ui/
│ │ └── shared/
│ └── admin/
│ └── src/
│ └── ...
└── packages/
├── ui/
│ ├── button/ # самостоятельный пакет UI-модуля
│ ├── input/ # самостоятельный пакет UI-модуля
│ └── modal/ # самостоятельный пакет UI-модуля
├── infra/
│ ├── theme/ # самостоятельный пакет infra-модуля
│ ├── backend-api/ # самостоятельный пакет infra-модуля
│ └── logger/ # самостоятельный пакет infra-модуля
└── shared/ # единый shared-пакет
├── package.json
└── src/
├── lib/ # переиспользуемые утилиты
├── helpers/ # переиспользуемые helpers
└── index.ts
```
`apps/{app}/src` — граница SLM-приложения. `packages/*` находятся выше SLM и не добавляют новые архитектурные слои.
## Группировка frontend-пакетов
Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: `ui`, `infra`, `shared`.
```text
packages/ui/* # пакеты UI-модулей
packages/infra/* # пакеты infra-модулей
packages/shared # единый shared-пакет
```
Эта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри `packages/`. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM.
## Пакет и модуль
Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации.
В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. `packages/shared` устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению.
```text
packages/ui/button/
packages/ui/modal/
packages/infra/theme/
packages/infra/backend-api/
packages/shared/
```
## Что остаётся в приложении
Слои `app`, `layouts`, `screens`, `widgets` и `business` остаются внутри конкретного приложения.
```text
apps/web/src/app/
apps/web/src/layouts/
apps/web/src/screens/
apps/web/src/widgets/
apps/web/src/business/
```
`app`, `layouts` и `screens` привязаны к роутингу, каркасу и страницам конкретного приложения. `widgets` не выносятся в пакеты, потому что это слой композиции интерфейса приложения.
`business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой.
## Что можно выносить
В пакеты выносится только код из `ui`, `infra` и `shared`, который потенциально будет использоваться в двух и более фронтенд-приложениях монорепозитория.
| Группа | Что выносить | Пример |
|--------|--------------|--------|
| `packages/ui/*` | Самостоятельные UI-модули без бизнес-логики | `packages/ui/button` |
| `packages/infra/*` | Самостоятельные технические сервисы | `packages/infra/backend-api` |
| `packages/shared` | Общие утилиты, helpers и фундаментальный код | `packages/shared` |
Пакет можно создавать сразу, если модуль имеет общую природу и ожидается его переиспользование между приложениями. App-specific код остаётся внутри приложения.
## UI-пакеты
В `packages/ui/*` размещаются переиспользуемые UI-модули.
```text
packages/ui/button/
├── package.json
└── src/
├── button.tsx
├── styles/
├── types/
└── index.ts
```
UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц.
## Infra-пакеты
В `packages/infra/*` размещаются переиспользуемые инфраструктурные модули.
```text
packages/infra/backend-api/
├── package.json
└── src/
├── clients/
├── config/
├── types/
└── index.ts
```
Привязанные к конкретному приложению сервисы остаются в `apps/{app}/src/infra`. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок.
## Shared-пакет
`packages/shared` является единым пакетом.
```text
packages/shared/
├── package.json
└── src/
├── lib/
├── helpers/
└── index.ts
```
В `packages/shared` сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте.
Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий `shared` не выносятся.
## Имена пакетов и импорты
Путь импорта задаётся `name` в `package.json`, а не расположением директории.
```json
{
"name": "@repo/theme"
}
```
```text
packages/infra/theme/package.json
```
```ts
import { ThemeProvider } from '@repo/theme'
```
Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены.
```ts
// Хорошо
import { Button } from '@repo/button'
// Плохо
import { Button } from '@repo/button/src/button'
```
## Зависимости
На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений.
```text
apps → packages
packages -/→ apps
```
Внутри приложения продолжает действовать обычное направление зависимостей SLM.
```text
app → [ layouts | screens ] → widgets → business → infra → ui → shared
```
Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях.
## Когда не выносить
Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API.
Фактическое использование в одном приложении не запрещает пакет, если модуль имеет общую природу и потенциально нужен нескольким приложениям.
```text
# Плохо
apps/web/src/screens/home/parts/promo-section/
packages/ui/promo-section/
```
Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным `parts/`-модулем.
## Конфигурационные пакеты
Конфигурационные пакеты не относятся к SLM-архитектуре.
Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в `packages/`, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри `src/`.
## Правила
- SLM применяется внутри каждого `apps/{app}/src`.
- Frontend-пакеты, вынесенные из SLM-приложений, группируются в `packages/ui`, `packages/infra`, `packages/shared`.
- Группы `packages/ui`, `packages/infra`, `packages/shared` не являются SLM-слоями.
- В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей.
- В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей.
- `packages/shared` является единым пакетом для переиспользуемых утилит и helpers.
- Модуль можно размещать в пакете, если он потенциально будет использоваться в двух и более фронтенд-приложениях.
- `business`, `app`, `layouts`, `screens`, `widgets` не выносятся в пакеты.
- Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`.
- Пакеты не импортируют приложения.
- Межпакетные импорты идут только через публичный API.
- Deep imports внутрь пакетов запрещены.
- Локальная колокация важнее преждевременного выноса в `packages/*`.

View File

@@ -0,0 +1,181 @@
---
title: Сегменты
description: Сегменты внутри модуля (ui/, model/, lib/ и др.), назначение и правила размещения файлов
---
# Сегменты
Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит.
## Определение
**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.**
## Обзор
| Сегмент | Содержимое |
|---------|------------|
| `ui/` | Презентационные компоненты родительского модуля |
| `parts/` | Вложенные модули со своими сегментами |
| `hooks/` | React-хуки |
| `stores/` | Сторы состояния |
| `services/` | Работа с внешними источниками данных |
| `mappers/` | Трансформация данных между форматами |
| `types/` | TypeScript-типы и интерфейсы |
| `styles/` | Стили |
| `lib/` | Утилиты и хелперы модуля |
| `config/` | Константы и конфигурация |
## Сегмент ui/
Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля.
Компонент в `ui/`:
- Находится в собственной папке.
- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`.
- Не содержит любые компоненты.
- Не содержит любые модули.
- Не импортирует код проекта за пределами родительского модуля.
- Не делает внешние запросы.
- Не вызывает сценарные хуки.
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
- Не содержит бизнес-логику или сценарную логику.
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](/architecture/modules#компонент).
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
```text
user/
├── ui/
│ ├── user-avatar/
│ │ ├── user-avatar.tsx
│ │ ├── styles/
│ │ │ └── user-avatar.module.css
│ │ ├── types/
│ │ │ └── user-avatar-props.type.ts
│ │ └── index.ts
│ └── user-status/
│ ├── user-status.tsx
│ └── index.ts
├── types/
├── hooks/
├── user.tsx
└── index.ts
```
Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `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/` — компонент родительского модуля без собственной архитектурной ответственности.
Вложенность `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
```

View File

@@ -0,0 +1,249 @@
---
title: Композиция через Provider
description: Пример композиции бизнес-фабрик screen-модуля через React Provider
---
# Композиция через Provider
Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя.
## Идея
Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние `parts/` достают нужные домены через хук без пропс-дриллинга.
## Принципы
1. **Принадлежность.** Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах.
2. **Внутренний тип.** Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей.
3. **Внутренний хук.** Хук доступа не экспортируется — доступен только внутри screen и его `parts/`.
4. **Публичный Provider.** Только Provider экспортируется через `index.ts`, чтобы роутер мог обернуть screen.
5. **Сборка снаружи.** Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает.
6. **Запрет для бизнеса.** Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики.
## Структура модуля
```text
screens/main/
├── main.screen.tsx
├── providers/
│ └── main-composition.provider.tsx
├── hooks/
│ └── use-main-composition.hook.ts
├── types/
│ └── main-composition.type.ts
├── parts/
│ └── featured-products/
│ ├── featured-products.tsx
│ └── index.ts
└── index.ts
```
Сегмент `providers/` — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются.
## Распределение по сегментам
| Файл | Сегмент | Назначение |
|------|---------|------------|
| `main-composition.type.ts` | `types/` | TypeScript-тип композиции |
| `main-composition.provider.tsx` | `providers/` | Context и Provider-компонент |
| `use-main-composition.hook.ts` | `hooks/` | React-хук доступа |
| `main.screen.tsx` | корень | Корневой компонент screen-модуля |
| `featured-products/` | `parts/` | Вложенный модуль со своим публичным API |
## Тип композиции
Файл: `screens/main/types/main-composition.type.ts`.
Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через `index.ts`, чтобы другие модули не зависели от внутренней формы композиции screen.
```ts
import type { CatalogApi } from '@/business/catalog'
import type { CartApi } from '@/business/cart'
export type MainComposition = {
catalog: CatalogApi
cart: CartApi
}
```
## Context и Provider
Файл: `screens/main/providers/main-composition.provider.tsx`.
Context — внутренняя деталь Provider, наружу он не экспортируется. Значение `null` по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве.
Provider-компонент экспортируется через `index.ts`. Роутер передаёт в `value` уже собранный граф фабрик со стабильной ссылкой.
```tsx
import { createContext, type ReactNode } from 'react'
import type { MainComposition } from '../types/main-composition.type'
export const MainCompositionContext = createContext<MainComposition | null>(null)
type Props = {
value: MainComposition
children: ReactNode
}
export const MainCompositionProvider = ({ value, children }: Props) => (
<MainCompositionContext.Provider value={value}>
{children}
</MainCompositionContext.Provider>
)
```
## Хук доступа
Файл: `screens/main/hooks/use-main-composition.hook.ts`.
Хук остаётся внутренним и не экспортируется через `index.ts` модуля. Он доступен только внутри screen и его `parts/`.
Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева.
```ts
import { useContext } from 'react'
import { MainCompositionContext } from '../providers/main-composition.provider'
export const useMainComposition = () => {
const ctx = useContext(MainCompositionContext)
if (!ctx) {
throw new Error('useMainComposition must be used within MainCompositionProvider')
}
return ctx
}
```
## Сборка графа в роутере
Файл: `app/router.tsx`.
Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики.
Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента.
```tsx
import { MainScreen, MainCompositionProvider } from '@/screens/main'
import { catalogFactory } from '@/business/catalog'
import { cartFactory } from '@/business/cart'
import { authFactory } from '@/business/auth'
const auth = authFactory()
const catalog = catalogFactory()
const cart = cartFactory({ auth })
const MainRoute = () => (
<MainCompositionProvider value={{ catalog, cart }}>
<MainScreen />
</MainCompositionProvider>
)
```
## Корневой компонент screen
Файл: `screens/main/main.screen.tsx`.
Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные `useCategories` и `CategoryList`, а не обращение к фабричному API через точку.
```tsx
import { useMainComposition } from './hooks/use-main-composition.hook'
import { FeaturedProducts } from './parts/featured-products'
export const MainScreen = () => {
const { catalog } = useMainComposition()
const { useCategories, CategoryList } = catalog
const categories = useCategories()
return (
<div>
<CategoryList categories={categories} />
<FeaturedProducts />
</div>
)
}
```
## Вложенный part
Файл: `screens/main/parts/featured-products/featured-products.tsx`.
Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props.
Из API доменов достаются готовые сущности: `useFeatured`, `ProductCard` и `addItem`. Компонент работает с ними напрямую.
```tsx
import { useMainComposition } from '../../hooks/use-main-composition.hook'
export const FeaturedProducts = () => {
const { catalog, cart } = useMainComposition()
const { useFeatured, ProductCard } = catalog
const { addItem } = cart
const products = useFeatured()
return (
<div>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
onAdd={() => addItem(product.id)}
/>
))}
</div>
)
}
```
Файл: `screens/main/parts/featured-products/index.ts`.
```ts
export { FeaturedProducts } from './featured-products'
```
## Публичный API screen-модуля
Файл: `screens/main/index.ts`.
Наружу экспортируются только screen и его Provider. `MainComposition`, `MainCompositionContext` и `useMainComposition` остаются деталями реализации.
```ts
export { MainScreen } from './main.screen'
export { MainCompositionProvider } from './providers/main-composition.provider'
```
## Почему тип композиции не экспортируется
Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen.
Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen.
```ts
import type { MainComposition } from '@/screens/main'
```
Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля.
## Почему хук не экспортируется
Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его `parts/`.
## Почему Provider экспортируется
Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева.
## Стабильность value
Фабрики создаются на уровне модуля, поэтому `catalog` и `cart` сохраняют ссылки между рендерами `MainRoute`.
Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию.
## Расширение на другие screen-модули
Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов.
```text
screens/checkout/providers/checkout-composition.provider.tsx
screens/checkout/hooks/use-checkout-composition.hook.ts
screens/checkout/types/checkout-composition.type.ts
```
Имена включают имя screen-модуля. Не используйте универсальные названия вроде `useComposition` или `useScope`: по имени файла должно быть понятно, к какой странице привязан Context.

View File

@@ -0,0 +1,52 @@
---
title: Композиция фабрик
description: Пример композиции business-фабрик на уровне screen-модуля в React-проекте
---
# Композиция фабрик
Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов.
## Идея
Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик.
## Структура screen-модуля
```text
screens/home/
├── home.screen.tsx
└── index.ts
```
## Сборка фабрик
Файл: `screens/home/home.screen.tsx`.
```tsx
import { customerFactory } from '@/business/customer'
import { orderFactory } from '@/business/order'
const customer = customerFactory()
const order = orderFactory({ customer })
const { useOrder, OrderCard } = order
export const HomeScreen = () => {
const currentOrder = useOrder()
return <OrderCard order={currentOrder} />
}
```
`customerFactory` создаётся первой, потому что `orderFactory` зависит от части API домена `customer`. Модуль `order` не импортирует `customer` в runtime — зависимость передаётся снаружи.
## Публичный API screen-модуля
Файл: `screens/home/index.ts`.
```ts
export { HomeScreen } from './home.screen'
```
Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля.

View File

@@ -0,0 +1,114 @@
---
title: Создание фабрики
description: Пример создания фабрики business-модуля в React-проекте
---
# Создание фабрики
Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API.
## Структура business-модуля
Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в `types/`.
```text
business/customer/
├── customer.factory.ts
├── hooks/
├── types/
│ ├── customer.type.ts
│ ├── customer-api.type.ts
│ └── customer-factory.type.ts
├── ui/
└── index.ts
```
## Тип публичного API
Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы.
```ts
// business/customer/types/customer-api.type.ts
import type { ReactNode } from 'react'
import type { Customer } from './customer.type'
export type CustomerCardProps = {
customer: Customer
}
export type CustomerApi = {
useCustomer: () => Customer | null
CustomerCard: (props: CustomerCardProps) => ReactNode
}
```
```ts
// business/customer/types/customer-factory.type.ts
import type { CustomerApi } from './customer-api.type'
export type CustomerFactory = () => CustomerApi
```
## Фабрика без зависимостей
Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов.
```ts
// business/customer/customer.factory.ts
import { useCustomer } from './hooks/use-customer.hook'
import { CustomerCard } from './ui/customer-card'
import type { CustomerFactory } from './types/customer-factory.type'
export const customerFactory: CustomerFactory = () => {
return {
useCustomer,
CustomerCard,
}
}
```
```ts
// business/customer/index.ts
export { customerFactory } from './customer.factory'
export type { Customer } from './types/customer.type'
export type { CustomerApi } from './types/customer-api.type'
export type { CustomerFactory } from './types/customer-factory.type'
```
## Фабрика с зависимостями
Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API.
```ts
// business/order/types/order-deps.type.ts
import type { CustomerApi } from '@/business/customer'
export type OrderDeps = {
customer: Pick<CustomerApi, 'useCustomer'>
}
```
```ts
// business/order/types/order-factory.type.ts
import type { OrderApi } from './order-api.type'
import type { OrderDeps } from './order-deps.type'
export type OrderFactory = (deps: OrderDeps) => OrderApi
```
```ts
// business/order/order.factory.ts
import { createUseOrder } from './hooks/use-order.hook'
import { OrderCard } from './ui/order-card'
import type { OrderFactory } from './types/order-factory.type'
export const orderFactory: OrderFactory = (deps) => {
const useOrder = createUseOrder(deps)
return {
useOrder,
OrderCard,
}
}
```

View File

@@ -0,0 +1,103 @@
---
title: Гид для агента
description: Что AI-агент обязан прочитать перед началом работы, а что — по задаче.
---
# Обязательное чтение перед началом работы
Этот документ определяет **строгий порядок действий агента перед выполнением любых задач**.
## Общее правило
Перед началом работы над **любой задачей** агент **обязан ознакомиться с базовой документацией проекта**.
Нарушение этого порядка считается ошибкой.
---
## Порядок обязательного чтения
Агент должен читать документацию **строго в следующем порядке**:
### 1. Архитектура (КРИТИЧЕСКИ ВАЖНО)
* [Архитектура: Обзор](./basics/architecture/index.md)
* [Архитектура: Слои](./basics/architecture/layers.md)
* [Архитектура: Модули](./basics/architecture/modules.md)
* [Архитектура: Сегменты](./basics/architecture/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. Задача пользователя
Если задача противоречит архитектуре — задача должна быть переосмыслена, а не выполнена напрямую.

59
canons/style-guide/MAP.md Normal file
View File

@@ -0,0 +1,59 @@
# Карта документации
Список всех разделов архива с относительными ссылками. Точка входа
`DEVELOP.md` рядом с этим файлом.
## Подсказки
- [Подсказки](./workflow.md) — Короткие ответы на типовые вопросы и решения для спорных ситуаций.
## Базовые правила
- [Технологии и библиотеки](./basics/tech-stack.md) — Какие библиотеки и инструменты используются в проекте.
- [Именование](./basics/naming.md) — Как называть переменные, файлы и прочие сущности в коде.
- [Архитектура: Обзор](./basics/architecture/index.md) — Архитектурный подход проекта: что такое SLM и как он устроен.
- [Архитектура: Слои](./basics/architecture/layers.md) — Из каких слоёв состоит SLM-архитектура и как они связаны.
- [Архитектура: Модули](./basics/architecture/modules.md) — Что такое модуль в SLM-архитектуре и как он устроен.
- [Архитектура: Сегменты](./basics/architecture/segments.md) — Что такое сегмент модуля в SLM-архитектуре и какие они бывают.
- [Стиль кода](./basics/code-style.md) — Как оформляется код в проекте.
- [Документирование](./basics/documentation.md) — Что и как документировать в коде.
- [Типизация](./basics/typing.md) — Как типизируется код в проекте.
## Прикладные разделы
- [Создание проекта: Из шаблона](./applied/creating-project/from-template.md) — Создание нового проекта на основе готового шаблона.
- [Создание проекта: По гайду вручную](./applied/creating-project/manual.md) — Поэтапное создание нового проекта без использования шаблона.
- [Создание проекта: Чистый Next.js](./applied/creating-project/nextjs.md) — Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку.
- [Структура проекта](./applied/project-structure.md) — Из чего состоит проект и где что лежит.
- [Страницы](./applied/page-level.md) — Как работать со страницами и другими файлами роутинга Next.js App Router.
- [Компонент](./applied/component.md) — Как создавать React-компоненты внутри SLM-модулей.
- [Модуль](./applied/module.md) — Как создавать и организовывать SLM-модули в проекте.
- [REST-клиент](./applied/rest-client/index.md) — Настройка REST-клиента сервиса для работы с внешним API.
- [REST-клиент: Настройка REST-клиента](./applied/rest-client/setup/index.md) — Из чего состоит REST-клиент и что подготовить перед использованием API.
- [REST-клиент: Автогенерация REST-клиента](./applied/rest-client/setup/auto.md) — Генерация REST-клиента из OpenAPI-спецификации.
- [REST-клиент: Ручное создание REST-клиента](./applied/rest-client/setup/manual.md) — Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
- [REST-клиент: GET-хуки REST-клиента](./applied/rest-client/setup/hooks.md) — Прозрачные SWR-обёртки над GET-методами REST-клиента.
- [REST-клиент: Использование REST-клиента](./applied/rest-client/usage.md) — Как вызывать готовый REST-клиент в серверном коде и submit-функциях.
- [Получение данных](./applied/data-fetch/index.md) — Как получать данные с учётом рендера страницы.
- [Получение данных: Серверный await](./applied/data-fetch/server-await.md) — Получение REST-данных на сервере до первого HTML.
- [Получение данных: Параллельные серверные запросы](./applied/data-fetch/parallel-server-requests.md) — Как запускать независимые REST-запросы на сервере без waterfall.
- [Получение данных: Передача промиса ниже](./applied/data-fetch/pass-promise-down.md) — Как запускать серверный REST-запрос выше и ожидать его во вложенном server-компоненте.
- [Получение данных: Начальные данные для клиентских хуков](./applied/data-fetch/client-hooks-initial-data.md) — Как дать клиентским GET-хукам начальные REST-данные.
- [Получение данных: Клиентский GET-хук](./applied/data-fetch/client-get-hook.md) — Получение REST-данных в Client Components через готовые GET-хуки REST-клиента.
- [Получение данных: Business-композиция](./applied/data-fetch/business-composition.md) — Когда REST-данные нужно объединить или интерпретировать в бизнес-модуле.
- [Стили: Настройка](./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) — Как организовать локализацию как infra-модуль.

View File

@@ -0,0 +1,77 @@
---
title: Алиасы импортов
description: Какие алиасы импортов есть в проекте и как ими пользоваться.
keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app, layouts, screens, widgets, business, infra, ui, shared]
---
# Алиасы импортов
Какие алиасы импортов есть в проекте и как ими пользоваться.
## Конфиг
`tsconfig.json` в корне проекта:
```json
{
"compilerOptions": {
"paths": {
"app/*": ["./src/app/*"],
"layouts/*": ["./src/layouts/*"],
"screens/*": ["./src/screens/*"],
"widgets/*": ["./src/widgets/*"],
"business/*": ["./src/business/*"],
"infra/*": ["./src/infra/*"],
"ui/*": ["./src/ui/*"],
"shared/*": ["./src/shared/*"]
}
}
}
```
Восемь алиасов — ровно по числу слоёв. Других алиасов в проекте нет.
## Правила
- **Каждый импорт между модулями — через алиас слоя.** Относительные пути (`../../`) запрещены за пределами своего модуля.
- **Внутри одного модуля** допустимы относительные импорты (`./model`, `./ui/button`) — это часть инкапсуляции модуля.
- **Префикс `@/` не используется.** Имя слоя — само по себе адрес.
- **Направление импортов** определяется архитектурой, не алиасами. Алиас разрешает импорт технически, но не отменяет правила слоёв (→ [Слои](/docs/basics/architecture/layers)).
**Хорошо**
```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'
```

View File

@@ -0,0 +1,114 @@
---
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` к стандартному виду (см. «Стандартный `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`, заменяется стандартным конфигом проекта.
Стандартный `biome.json`:
```jsonc
{
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true,
"includes": ["**", "!node_modules", "!.next", "!dist", "!build", "!.templates", "!src/infra/**/generated"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 120
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double"
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noUnknownAtRules": "off"
},
"correctness": {
"noUnknownMediaFeatureName": "off"
}
},
"domains": {
"next": "recommended",
"react": "recommended"
}
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
```
`src/infra/**/generated` исключается из Biome, потому что generated-файлы не правятся руками. При этом generated-файлы остаются в git.
Правила `suspicious/noUnknownAtRules` и `correctness/noUnknownMediaFeatureName` отключены, потому что проектный CSS-стек использует `@custom-media` и другие конструкции, которые Biome может не распознавать.
## Интеграция с VS Code
Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [VS Code](/docs/applied/vscode).

View File

@@ -0,0 +1,165 @@
---
title: Компонент
description: Как должен выглядеть сгенерированный React-компонент внутри SLM-модуля.
---
# Компонент
Как должен выглядеть сгенерированный React-компонент внутри SLM-модуля.
## Назначение
Архитектурное определение компонента описано в разделе [Модули → Компонент](/docs/basics/architecture/modules#компонент), а структура сегмента `ui/` — в разделе [Сегменты → ui/](/docs/basics/architecture/segments#сегмент-ui).
Эта страница не повторяет архитектурные ограничения. Она показывает, каким должен быть результат генерации компонента: структура папки, `.tsx`, типы, стили и локальный экспорт.
::: danger Компоненты не создаются вручную
Компоненты в проекте создаются только через кодогенератор: через [VS Code](/docs/applied/templates/templates-usage#через-vs-code) или [CLI](/docs/applied/templates/templates-usage#через-cli).
Ручное создание компонента запрещено. Это грубое нарушение правил работы в проекте для разработчика и AI-ассистента.
Если в проекте нет шаблона `.templates/component`, сначала создайте шаблон по разделу [Создание шаблонов](/docs/applied/templates/templates-create), и только потом генерируйте компонент на его основе.
:::
## Создание
1. Проверьте, что в проекте есть шаблон `.templates/component`.
2. Если шаблона нет — создайте его по разделу [Создание шаблонов](/docs/applied/templates/templates-create).
3. Сгенерируйте компонент через [VS Code или CLI](/docs/applied/templates/templates-usage).
Структура и код ниже показывают ожидаемый результат генерации. Их нельзя использовать как инструкцию для ручного создания файлов.
## Структура
Компонент размещается в `ui/{component-name}/` родительского модуля.
Для каждого компонента обязательны `.tsx`, типы, стили и локальный `index.ts`.
```text
user-card/
└── ui/
└── user-status/
├── styles/
│ └── user-status.module.css
├── types/
│ └── user-status-props.type.ts
├── user-status.tsx
└── index.ts
```
## Реализация
Пример ниже показывает файлы базового компонента.
### Типы
Файл типов делится на три части:
- `UserStatusParams` — собственные параметры компонента. Здесь лежат только данные, которые нужны именно этому компоненту.
- `RootAttrs` — параметры корневой обёртки: `div`, `span`, `a`, `button` или другого HTML-элемента. Если компонент сам управляет `children`, они исключаются через `Omit`.
- `UserStatusProps` — итоговые пропсы компонента. Тип объединяет собственные параметры и параметры корневой обёртки.
Собственные параметры и их поля документируются по правилам раздела [Документирование → Типы, интерфейсы, enum](/docs/basics/documentation#типы-интерфейсы-enum).
`user-card/ui/user-status/types/user-status-props.type.ts`
```ts
import type { ComponentPropsWithoutRef } from 'react'
/**
* Параметры UserStatus.
*/
export type UserStatusParams = {
/** Текст статуса пользователя. */
label: string
/** Доступен ли пользователь сейчас. */
isOnline: boolean
}
/** Атрибуты корневого элемента без children. */
type RootAttrs = Omit<ComponentPropsWithoutRef<'span'>, 'children'>
export type UserStatusProps = RootAttrs & UserStatusParams
```
### TSX
В `.tsx` лежит только сам компонент:
- Компонент объявляется через `const` и именованный экспорт.
- `React.FC` не используется.
- Параметры компонента типизируются через `Props`.
- Возвращаемый тип не указывается: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата.
- JSDoc-комментарий обязателен и пишется по правилам раздела [Документирование → Компоненты](/docs/basics/documentation#компоненты).
- Пропсы деструктурируются в теле компонента, а не в сигнатуре.
- Из пропсов обязательно выделяются `className` и `...rootAttrs`.
- Функция конкатенации CSS-классов импортируется и именуется `cl`.
- Корневой CSS-класс всегда называется `.root`.
Комментарий описывает назначение и сценарии применения компонента, а не DOM-разметку или внутреннюю реализацию.
`className` — внешний CSS-класс, который родитель может передать компоненту. `rootAttrs` — остальные атрибуты корневой обёртки: `id`, `aria-*`, `data-*`, обработчики событий и другие HTML-атрибуты. Они прокидываются на корневой DOM-элемент компонента.
`.root` нужен, чтобы в DevTools быстро находить корневой DOM-узел компонента и одинаково подключать внешний `className` к реальному корню.
`user-card/ui/user-status/user-status.tsx`
```tsx
import cl from 'clsx'
import type { UserStatusProps } from './types/user-status-props.type'
import styles from './styles/user-status.module.css'
/**
* Статус пользователя в карточке профиля.
*
* Используется для:
* - отображения текущей доступности пользователя
* - визуального выделения онлайн- и офлайн-состояний
*/
export const UserStatus = (props: UserStatusProps) => {
const { label, isOnline, className, ...rootAttrs } = props
return (
<span
{...rootAttrs}
className={cl(styles.root, isOnline && styles.online, className)}
>
{label}
</span>
)
}
```
### Стили
`user-card/ui/user-status/styles/user-status.module.css`
```css
.root {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--color-text-muted);
}
.root::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.online {
color: var(--color-success);
}
```
### Локальный экспорт
`user-card/ui/user-status/index.ts`
```ts
export { UserStatus } from './user-status'
export type { UserStatusProps } from './types/user-status-props.type'
```

View File

@@ -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/` — решение о репозитории принимает разработчик.

View File

@@ -0,0 +1,90 @@
---
title: Создание проекта вручную
description: Поэтапное создание нового проекта без использования шаблона.
keywords: [создать проект, новый проект, с нуля, init, initialize, scaffold, create-next-app, начать проект, поднять проект, эталонный проект, ручная установка]
---
# Создание проекта вручную
Поэтапное создание нового проекта без использования шаблона.
## Состав эталонного проекта
| Компонент | Роль | Раздел |
|-----------|------|--------|
| Next.js | Фреймворк (роутинг, сборка, SSR) | [Next.js](/docs/applied/creating-project/nextjs) |
| Алиасы | Импорты по слоям SLM | [Алиасы](/docs/applied/aliases) |
| Biome | Линтер и форматтер (замена ESLint + Prettier) | [Biome](/docs/applied/biome) |
| Стили | Глобальные токены и breakpoints | [Стили](/docs/applied/styles/styles-setup) |
| PostCSS | CSS-процессор для custom-media и вложенности | [PostCSS](/docs/applied/postcss) |
| SVG-спрайты | Иконки через `<SvgSprite/>`, управление цветом | [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-setup) |
| VS Code | Настройки редактора и расширения | [VS Code](/docs/applied/vscode) |
| Шаблоны генерации | `.templates/` для `@gromlab/create` | [Шаблоны генерации](/docs/applied/templates/templates-setup) |
Убрать компонент из состава — значит согласованно отказаться от части стайлгайда. Частичные проекты возможны (только Next.js, Next.js + стили и т.п.), но не являются эталоном.
## Канон раскладки
В `src/` допустимы только слои SLM: `app/`, `layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)).
В частности: `src/app/` содержит только файлы роутинга Next.js и инициализации, без каталогов `styles/`, `assets/`, `components/`.
## Порядок установки
Подсистемы ставятся в фиксированном порядке — он отражает зависимости между шагами.
### 1. Next.js
Скелет фреймворка — обязательный первый шаг, остальное опирается на него.
См. [Next.js](/docs/applied/creating-project/nextjs). После выполнения проверки этого раздела `npm run build` должен проходить.
### 2. Алиасы
Заменить дефолтный `"@/*"` в `tsconfig.json` на канонический список из восьми слой-префиксов.
См. [Алиасы](/docs/applied/aliases).
### 3. Biome
Линтер и форматтер. Подключается **до** написания кода, иначе в проекте копятся несогласованные правки.
См. [Biome](/docs/applied/biome).
### 4. Стили (базовая инфраструктура)
Файлы `variables.css`, `media.css`, `global.css` в `src/shared/styles/` и подключение `global.css` в `src/app/layout.tsx`. CSS-процессор на этом шаге не ставится.
См. [Стили](/docs/applied/styles/styles-setup).
### 5. PostCSS
CSS-процессор поверх базовых стилей: `@custom-media`, вложенность, autoprefixer. Ставится **только после шага 4** — опирается на `src/shared/styles/media.css`.
См. [PostCSS](/docs/applied/postcss).
### 6. SVG-спрайты
Пакет `@gromlab/svg-sprites`, генерация спрайт-файла и React-компонента `<SvgSprite/>`.
См. [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-setup).
### 7. VS Code
Расширения и настройки редактора. Опирается на установленный Biome (форматирование при сохранении) и PostCSS (ассоциация `*.css`).
См. [VS Code](/docs/applied/vscode).
### 8. Шаблоны генерации
Папка `.templates/` для генератора модулей `@gromlab/create`.
См. [Шаблоны генерации](/docs/applied/templates/templates-setup).
## Правила
- **Порядок шагов фиксирован.** Перестановка ломает зависимости (PostCSS требует базовых стилей, VS Code — установленного Biome).
- **Между шагами обязательна проверка** из соответствующего раздела. Не переходить дальше, пока чеклист текущего шага не пройден.
- **Слои `src/`** (`layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`) не создавать авансом. Появляются по мере первого модуля. Исключения — `src/app/` (создаётся `create-next-app`), `src/shared/styles/` (шаг 1) и `src/shared/sprites/icons/` (шаг 6).
- **Посторонние каталоги в `src/`** (`assets/`, `utils/`, `lib/`, `components/` и т.п.) — запрещены.
- **Подмножество шагов допустимо.** Можно ставить только Next.js и часть инструментов; полный набор — это эталон, а не обязательство.

View File

@@ -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 ([Типизация](/docs/basics/typing)) |
| `--app` | App Router | Pages Router не используется |
| `--src-dir` | Код в `src/` | Архитектура SLM требует `src/` ([Структура проекта](/docs/applied/project-structure)) |
| `--import-alias "@/*"` | Placeholder | Требуется флагом; после установки `paths` полностью переписывается на слой-префиксы (см. [Алиасы](/docs/applied/aliases)) |
| `--no-eslint` | ESLint не ставится | Линтер и форматтер — Biome ([Biome](/docs/applied/biome)) |
| `--no-tailwind` | Tailwind не ставится | Стилизация — PostCSS Modules ([Стили](/docs/applied/styles/styles-usage)) |
| `--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 <h1>Home</h1>
}
```
Очистить `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 (
<html lang="ru">
<body>{children}</body>
</html>
)
}
```
### 3. Создать папку `src/shared/styles/`
Глобальные стили в SLM-архитектуре живут в слое `shared`, а не в `src/app/` ([Структура проекта](/docs/applied/project-structure)).
```bash
mkdir -p src/shared/styles
```
Остальные слои (`layouts/`, `screens/`, `widgets/`, `business/`, `infra/`, `ui/`) заводятся при появлении первого модуля в них. `src/shared/styles/` — единственный подкаталог `shared/`, который заводится сразу: без него не настроить стили на следующих шагах.
## Правила
- **Конфликт с непустой директорией** — не удалять файлы пользователя автоматически. Ставить в подпапку или временно перенести посторонние файлы.
- **Отклонение от канонических флагов** (pnpm, Tailwind, ESLint и т.п.) — только осознанное, с пониманием, что стайлгайд этого не предусматривает.
- **Слои `src/`** не создавать авансом — появляются при первом модуле. Алиасы прописываются сразу на все восемь слоёв (см. [Алиасы](/docs/applied/aliases)).
- **`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`).

View File

@@ -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.
Такая логика не пишется в `infra/`. REST-клиент остаётся прозрачным адаптером к API.
## Пример поверх одного GET-хука
```ts
// src/business/pets/hooks/use-available-pets.hook.ts
import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
/**
* Доменный список доступных питомцев.
*/
export const useAvailablePets = () => {
const query = useGetPetList({ status: StatusEnum.Available })
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
```
`useGetPetList` — infra-хук. `hasPets` — бизнес-интерпретация, поэтому она появляется в `business/pets`.
## Пример композиции нескольких GET-хуков
```ts
// src/business/pets/hooks/use-pets-dashboard.hook.ts
import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
/**
* Данные dashboard по питомцам.
*/
export const usePetsDashboard = () => {
const availablePets = useGetPetList({ status: StatusEnum.Available })
const pendingPets = useGetPetList({ status: StatusEnum.Pending })
const soldPets = useGetPetList({ status: StatusEnum.Sold })
return {
availablePets,
pendingPets,
soldPets,
total:
(availablePets.data?.length ?? 0) +
(pendingPets.data?.length ?? 0) +
(soldPets.data?.length ?? 0),
}
}
```
Композиция нескольких запросов не добавляется в `infra/pet-store-api/hooks/`, потому что это уже сценарий потребления данных.
## Пример auth-состояния
```ts
// src/business/auth/hooks/use-auth-state.hook.ts
import { useGetCurrentUser } from 'infra/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-смысл внутри infra-хука
export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
const query = useSWR(...)
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
```
REST-модуль отвечает за доступ к API. Business-модуль отвечает за смысл этих данных в продукте.

View File

@@ -0,0 +1,88 @@
---
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 { StatusEnum, useGetPetList } from 'infra/pet-store-api'
const statuses = [StatusEnum.Available, StatusEnum.Pending, StatusEnum.Sold]
export function PetTabs() {
const [status, setStatus] = useState(StatusEnum.Available)
const { data: pets, isLoading, error } = useGetPetList({ status })
return (
<section>
<div>
{statuses.map((item) => (
<button key={item} type="button" onClick={() => setStatus(item)}>
{item}
</button>
))}
</div>
{isLoading && <div>Загрузка...</div>}
{error && <div>Ошибка загрузки</div>}
<ul>
{pets?.map((pet) => (
<li key={pet.id}>{pet.name}</li>
))}
</ul>
</section>
)
}
```
Компонент выбирает параметр `status`, но не знает про SWR-ключ и fetcher. Запрос выполняет готовый GET-хук REST-клиента.
## Если хука нет
Хук добавляется в REST-модуль сервиса:
```text
src/infra/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/findByStatus?status=${status}`],
() => petStoreApi.pet.findPetsByStatus({ status }),
)
```
Такой код теряет единое место для ключей, дублирует fetcher и разносит инфраструктурные детали по UI.
## Когда выбрать другую стратегию
- Данные нужны до первого HTML — [Серверный await](/docs/applied/data-fetch/server-await).
- Клиентский хук должен получить начальные данные сразу — [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data).
- Нужно вычислить бизнес-состояние — [Business-композиция](/docs/applied/data-fetch/business-composition).

View File

@@ -0,0 +1,117 @@
---
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/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
if (!params?.status) {
return null
}
return ['pet-store-api', `/pet/findByStatus?status=${params.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,
StatusEnum,
} from 'infra/pet-store-api'
type PetsLayoutProps = {
children: ReactNode
}
export default async function PetsLayout({ children }: PetsLayoutProps) {
const params = { status: StatusEnum.Available }
const availablePetsPromise = petStoreApi.pet.findPetsByStatus(params)
return (
<SWRConfig
value={{
fallback: {
[unstable_serialize(getPetListKey(params))]: availablePetsPromise,
},
}}
>
{children}
</SWRConfig>
)
}
```
Если GET-хук использует array-key, ключ для `fallback` сериализуется через `unstable_serialize`.
## Клиентский компонент
```tsx
'use client'
import { StatusEnum, useGetPetList } from 'infra/pet-store-api'
export function PetList() {
const { data: pets, isLoading } = useGetPetList({
status: StatusEnum.Available,
})
if (isLoading) return <div>Загрузка...</div>
return (
<ul>
{pets?.map((pet) => (
<li key={pet.id}>{pet.name}</li>
))}
</ul>
)
}
```
Компонент не знает, что данные были запущены на сервере. Он использует обычный GET-хук REST-клиента.
## Что важно
- Ключ `fallback` должен совпадать с ключом GET-хука.
- `fallback` использует ту же key-функцию и те же params, что и GET-хук.
- Серверный код вызывает метод клиента, а не GET-хук.
- Клиентский компонент вызывает GET-хук, а не `useSWR` напрямую.
- Эта стратегия не означает ручную работу с кешем в компонентах.
## Когда не использовать
Если данные нужны только серверному компоненту, используйте [Серверный await](/docs/applied/data-fetch/server-await). Если данные зависят от состояния браузера, используйте [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook).

View File

@@ -0,0 +1,100 @@
---
title: Получение данных
description: Как получать данные с учётом рендера страницы.
keywords: [rest, стратегии, render, isr, ssr, server components, swr, promise, business]
---
# Получение данных
Как получать данные с учётом рендера страницы.
Перед выбором стратегии должен быть настроен REST-клиент сервиса. Если клиента ещё нет, начните с раздела [Настройка REST-клиента](/docs/applied/rest-client/setup/).
## Сначала определите рендер страницы
В 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](/docs/applied/data-fetch/server-await) |
| Несколько независимых данных нужны до рендера | Запуск промисов + `Promise.all` | [Параллельные серверные запросы](/docs/applied/data-fetch/parallel-server-requests) |
| Часть UI можно загрузить отдельно | Передача промиса ниже + `Suspense` | [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down) |
| Client Component должен получить данные сразу из SWR | Начальные данные для клиентских хуков | [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data) |
| Данные зависят от client state | Клиентский GET-хук | [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook) |
| Нужно объединить несколько запросов или вычислить `isAuth`, `canEdit`, `hasPets` | Business-композиция | [Business-композиция](/docs/applied/data-fetch/business-composition) |
## Правило выбора
Не выбирайте стратегию по любимому инструменту. Выбирайте её по двум вопросам:
```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/findByStatus?status=available'],
() => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }),
)
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
return {
...query,
hasPets: Boolean(query.data?.length),
}
```
Не отключайте ISR без причины. В компонентах используются готовые методы клиента или готовые хуки. SWR-ключи, fetcher и транспорт остаются внутри REST-модуля.

View File

@@ -0,0 +1,94 @@
---
title: Параллельные серверные запросы
description: Как запускать независимые REST-запросы на сервере без waterfall.
keywords: [rest, promise.all, параллельные запросы, server components]
---
# Параллельные серверные запросы
Если серверному компоненту нужно несколько независимых данных, запускайте запросы до ожидания результата. Последовательный `await` создаёт waterfall и замедляет рендер.
## Когда использовать
- Запросы независимы друг от друга.
- Все данные нужны текущему серверному компоненту перед возвратом UI.
- Нельзя или не нужно стримить часть UI отдельно.
## Хорошо
```tsx
import { petStoreApi, StatusEnum } from 'infra/pet-store-api'
import { PetsDashboardScreen } from 'screens/pets-dashboard'
export default async function PetsDashboardPage() {
const availablePetsPromise = petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Available,
})
const pendingPetsPromise = petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Pending,
})
const soldPetsPromise = petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Sold,
})
const [availablePets, pendingPets, soldPets] = await Promise.all([
availablePetsPromise,
pendingPetsPromise,
soldPetsPromise,
])
return (
<PetsDashboardScreen
availablePets={availablePets}
pendingPets={pendingPets}
soldPets={soldPets}
/>
)
}
```
## Плохо
```tsx
export default async function PetsDashboardPage() {
const availablePets = await petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Available,
})
const pendingPets = await petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Pending,
})
const soldPets = await petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Sold,
})
return (
<PetsDashboardScreen
availablePets={availablePets}
pendingPets={pendingPets}
soldPets={soldPets}
/>
)
}
```
Во втором примере каждый следующий запрос ждёт предыдущий, хотя они независимы.
## Зависимые запросы
Если второй запрос зависит от результата первого, последовательный `await` допустим:
```tsx
export default async function OrderPage({ params }: OrderPageProps) {
const { id } = await params
const order = await petStoreApi.store.getOrderById({ orderId: Number(id) })
const pet = await petStoreApi.pet.getPetById({ petId: order.petId })
return <OrderScreen order={order} pet={pet} />
}
```
Не превращайте зависимый сценарий в `Promise.all` искусственно.
## Когда выбрать другую стратегию
Если часть данных не обязательна для первого блока UI, можно запустить промис выше и передать его ниже: [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down).

View File

@@ -0,0 +1,64 @@
---
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, StatusEnum } from 'infra/pet-store-api'
import { PetListSection } from 'widgets/pet-list-section'
import { PetListSkeleton } from 'widgets/pet-list-section'
import type { Pet } from 'infra/pet-store-api'
export default function PetsPage() {
const petsPromise = petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Available,
})
return (
<main>
<h1>Питомцы</h1>
<Suspense fallback={<PetListSkeleton />}>
<AvailablePets petsPromise={petsPromise} />
</Suspense>
</main>
)
}
async function AvailablePets({ petsPromise }: { petsPromise: Promise<Pet[]> }) {
const pets = await petsPromise
return <PetListSection pets={pets} />
}
```
Запрос стартует в `PetsPage`, но ожидание происходит внутри `AvailablePets`. `Suspense` управляет fallback для этой части UI.
## Граница стратегии
Эта стратегия остаётся серверной. Не используйте её как замену GET-хукам в Client Components.
Если данные должны попасть в клиентский SWR-хук, используйте [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data).
## Что не делать
```tsx
// Плохо — передавать промис в произвольный клиентский компонент без ясной стратегии
return <PetListClient petsPromise={petsPromise} />
```
Для клиентского потребления есть отдельная стратегия через `SWRConfig fallback` и готовые GET-хуки REST-клиента.

View File

@@ -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, StatusEnum } from 'infra/pet-store-api'
import { PetsScreen } from 'screens/pets'
export default async function PetsPage() {
const pets = await petStoreApi.pet.findPetsByStatus({
status: StatusEnum.Available,
})
return <PetsScreen pets={pets} />
}
```
`page.tsx` получает данные первого рендера и передаёт их ниже. UI страницы остаётся в `screens/`, а не пишется прямо в `app/`.
## Пример детальной страницы
```tsx
// src/app/(routes)/pets/[id]/page.tsx
import { notFound } from 'next/navigation'
import { petStoreApi } from 'infra/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({ petId: Number(id) }).catch(() => null)
if (!pet) {
notFound()
}
return <PetDetailScreen pet={pet} />
}
```
Обработка 404 зависит от API-клиента и класса ошибок. В примере показана идея: решение о `notFound()` принимается на уровне маршрута, а не внутри REST-клиента.
## Что не делать
```tsx
// Плохо — хуки нельзя вызывать в Server Component
const { data } = useGetPetList({ status: StatusEnum.Available })
// Плохо — прямой fetch в обход клиента
const response = await fetch('https://petstore3.swagger.io/api/v3/pet/findByStatus')
```
Если данные нужны на сервере, вызывайте метод REST-клиента напрямую.
## Когда выбрать другую стратегию
- Несколько независимых запросов — [Параллельные серверные запросы](/docs/applied/data-fetch/parallel-server-requests).
- Часть UI можно грузить отдельно — [Передача промиса ниже](/docs/applied/data-fetch/pass-promise-down).
- Данные нужны клиентскому хуку сразу после гидрации — [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data).

View File

@@ -0,0 +1,128 @@
---
title: Шрифты
description: Как подключать шрифты через Next.js Font в проекте.
---
# Шрифты
Как подключать шрифты через Next.js Font в проекте.
## Назначение
Шрифты подключаются через `next/font`. Это стандартный способ Next.js: шрифты загружаются без ручных `<link>`, `@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 (
<html lang="ru" className={inter.variable}>
<body>{children}</body>
</html>
)
}
```
```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 (
<html lang="ru" className={roboto.variable}>
<body>{children}</body>
</html>
)
}
```
Путь в `localFont` указывается относительно `.font.ts`, поэтому файлы шрифта импортируются коротко: `./Roboto-Regular.woff2`.
Если шрифтов несколько, у каждого своя папка и свой `.font.ts`.
## Правила
- Использовать `next/font/google` или `next/font/local`.
- Не подключать шрифты через ручные `<link>` и `@font-face` без необходимости.
- Подключать шрифты один раз — в корневом layout через готовый объект шрифта.
- Использовать CSS-переменные `variable`, а не жёстко прописывать семейство в каждом компоненте.
- Локальные файлы шрифтов хранить в `src/shared/fonts/{font-name}/` рядом с `{font-name}.font.ts`.
- Не объявлять `localFont` внутри `src/app/layout.tsx`; layout только импортирует готовый шрифт.

View File

@@ -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
src="/images/user-avatar.png"
alt="Аватар пользователя"
width={96}
height={96}
/>
)
}
```
## Правила
- Использовать `Image` из `next/image`, не обычный `<img>`.
- Для контентных изображений всегда писать осмысленный `alt`.
- Для декоративных изображений использовать `alt=""`.
- Указывать `width` и `height`, если изображение не использует `fill`.
- При `fill` задавать `sizes` и контролировать размеры родителя стилями.
- `priority` ставить только для изображений первого экрана.
- SVG-иконки не оформлять как изображения — для них используется раздел [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-intro).
## Пример с `fill`
```tsx
import Image from 'next/image'
import styles from '../styles/article-card-cover.module.css'
export const ArticleCardCover = () => {
return (
<div className={styles.root}>
<Image
src="/images/article-cover.jpg"
alt="Обложка статьи"
fill
sizes="(min-width: 768px) 33vw, 100vw"
/>
</div>
)
}
```
```css
.root {
position: relative;
aspect-ratio: 16 / 9;
overflow: hidden;
}
```

View File

@@ -0,0 +1,81 @@
---
title: Локализация
description: Как организовать локализацию как infra-модуль.
---
# Локализация
Как организовать локализацию как infra-модуль.
## Назначение
Локализация — инфраструктурная подсистема приложения. Она отвечает за текущую локаль, словари, форматирование переводов и API для компонентов.
Код локализации живёт в `src/infra/i18n/`. Компоненты и модули не читают словари напрямую — они используют публичный API infra-модуля.
## Структура
```text
src/infra/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
```
Набор сегментов может отличаться, но публичная точка входа остаётся одна — `infra/i18n`.
## Подключение
`app/` только подключает готовый провайдер локализации. Реализация провайдера, словари и конфиг остаются в `infra/i18n/`.
```tsx
// src/app/layout.tsx
import type { ReactNode } from 'react'
import { I18nProvider } from 'infra/i18n'
type RootLayoutProps = {
children: ReactNode
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="ru">
<body>
<I18nProvider locale="ru">{children}</I18nProvider>
</body>
</html>
)
}
```
## Использование
Компоненты получают переводы через готовый API модуля локализации:
```tsx
import { useTranslation } from 'infra/i18n'
export const ProfileTitle = () => {
const { t } = useTranslation()
return <h1>{t('profile.title')}</h1>
}
```
## Правила
- Локализация живёт в `infra/i18n/`.
- `app/` только подключает готовый provider и передаёт locale.
- Словари не импортируются напрямую в компоненты, screens или business-модули.
- Ключи переводов не собираются динамически из строк, если это ломает типизацию и поиск.
- Тексты интерфейса не хардкодятся в переиспользуемых компонентах, если они должны переводиться.
- Форматирование дат, чисел и валют должно проходить через API локализации или отдельные утилиты infra-модуля.

View File

@@ -0,0 +1,156 @@
---
title: Модуль
description: Как должен выглядеть сгенерированный SLM-модуль в проекте.
---
# Модуль
Как должен выглядеть сгенерированный SLM-модуль в проекте.
## Назначение
Архитектурное определение модуля описано в разделе [Архитектура → Модули](/docs/basics/architecture/modules). Список сегментов описан в разделе [Архитектура → Сегменты](/docs/basics/architecture/segments).
Эта страница показывает прикладное оформление трёх типов модулей: UI, бизнес и инфраструктурный.
## Создание
1. Проверьте, что в проекте есть нужный шаблон в `.templates/`.
2. Если шаблона нет — создайте его по разделу [Создание шаблонов](/docs/applied/templates/templates-create).
3. Сгенерируйте модуль через [VS Code или CLI](/docs/applied/templates/templates-usage).
## Типы модулей
Архитектура определяет три типа модулей ([Типы модулей](/docs/basics/architecture/modules#типы-модулей)):
| Тип | Обязательный файл | Описание |
|---|---|---|
| UI-модуль | `{name}.tsx` | Модуль, выросший из компонента |
| Бизнес-модуль | `{name}.factory.ts` | Модуль вокруг публичного runtime API |
| Инфраструктурный модуль | нет | Модуль вокруг технического сервиса |
## UI-модуль
UI-модуль — это компонент, который перерос ограничения компонента: получил собственные хуки, вложенные модули в `parts/`, сценарную логику или публичный API. Внутренняя структура та же, что у компонента: корневой `.tsx`, типы, стили, `ui/`. Но без ограничений компонента.
Подробное оформление компонентов внутри `ui/` описано в разделе [Компонент](/docs/applied/component).
## Бизнес-модуль
Бизнес-модуль строится вокруг публичного runtime API. Ключевой файл — фабрика (`{name}.factory.ts`), которая возвращает всё, что нужно внешнему коду в runtime.
Архитектурное описание фабрики: [Архитектура → Фабрика](/docs/basics/architecture/modules#фабрика).
### Структура
```text
business/customer/
├── customer.factory.ts
├── index.ts
└── types/
├── customer.type.ts
├── customer-api.type.ts
├── customer-deps.type.ts
└── customer-factory.type.ts
```
### Типы
`business/customer/types/customer-api.type.ts`
```ts
export type CustomerApi = {
useCustomer: () => Customer
CustomerCard: (props: CustomerCardProps) => ReactNode
}
```
`business/order/types/order-deps.type.ts`
```ts
export type OrderDeps = {
customer: Pick<CustomerApi, 'useCustomer'>
}
```
`business/order/types/order-factory.type.ts`
```ts
export type OrderFactory = (deps: OrderDeps) => OrderApi
```
### Фабрика без зависимостей
`business/customer/customer.factory.ts`
```ts
import type { CustomerFactory } from './types/customer-factory.type'
export const customerFactory: CustomerFactory = () => {
return {
useCustomer,
CustomerCard,
}
}
```
### Фабрика с зависимостями
`business/order/order.factory.ts`
```ts
import type { OrderFactory } from './types/order-factory.type'
export const orderFactory: OrderFactory = (deps) => {
return {
useOrder,
OrderCard,
}
}
```
### Композиция на уровне screen
```tsx
// screens/home/home.screen.tsx
import { customerFactory } from '@/business/customer'
import { orderFactory } from '@/business/order'
const customer = customerFactory()
const order = orderFactory({ customer })
const { useOrder, OrderCard } = order
export const HomeScreen = () => {
const currentOrder = useOrder()
return <OrderCard order={currentOrder} />
}
```
## Инфраструктурный модуль
Инфраструктурный модуль строится вокруг технического сервиса или интеграции. Его структура определяется природой сервиса — фиксированного корневого файла нет.
Архитектурное описание: [Архитектура → Типы модулей → Инфраструктурный модуль](/docs/basics/architecture/modules#инфраструктурный-модуль).
Пример модуля темы:
```text
theme/
├── index.ts
├── config/
├── hooks/
├── styles/
└── ui/
```
Пример модуля API-клиента:
```text
backend-api/
├── backend-api.client.ts
├── config/
├── types/
└── index.ts
```

View File

@@ -0,0 +1,186 @@
---
title: Файлы роутинга
description: Как работать со страницами и другими файлами роутинга Next.js App Router.
---
# Файлы роутинга
Как работать со страницами и другими файлами роутинга Next.js App Router.
## Назначение
`src/app/**` — точка входа приложения и слой файлового роутинга Next.js.
Файлы роутинга не реализуют интерфейс. Они описывают маршрут: читают параметры, получают данные первого рендера, подготавливают кеш или состояние и передают результат в screen.
Границы слоя описаны в [Архитектура → Слои → App](/docs/basics/architecture/layers#слой-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/`, `infra/`, `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 <ProfileScreen id={id} />
}
```
## Данные первого рендера
Если данные нужны до первого рендера, `page.tsx` получает их на сервере и передаёт в screen. Сам запрос выполняется через готовый клиент или сервис нижнего слоя.
```tsx
import { notFound } from 'next/navigation'
import { userApi } from 'infra/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 <UserScreen user={user} />
}
```
Если данные нужны нескольким клиентским 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 'infra/backend-api'
type FeedLayoutProps = {
children: ReactNode
}
export default async function FeedLayout({ children }: FeedLayoutProps) {
const userPromise = backendApi.user.getCurrent()
const postsPromise = backendApi.posts.list()
return (
<SWRConfig
value={{
fallback: {
[unstable_serialize(getCurrentUserKey())]: userPromise,
[unstable_serialize(getPostListKey())]: postsPromise,
},
}}
>
{children}
</SWRConfig>
)
}
```
Подробнее о стратегиях запросов и начальных данных для клиентских хуков: [Получение данных](/docs/applied/data-fetch/), [Начальные данные для клиентских хуков](/docs/applied/data-fetch/client-hooks-initial-data).
## Инициализация состояния
Файл роутинга может подключить готовый провайдер стора страницы, если состояние зависит от маршрута или данных первого рендера. Реализация стора и провайдера не размещается в `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 (
<ProfileStoreProvider initialState={{ userId: id }}>
<ProfileScreen />
</ProfileStoreProvider>
)
}
```
## 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 <ErrorScreen error={error} reset={reset} />
}
export default ErrorPage
```

View File

@@ -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` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование стилей](/docs/applied/styles/styles-usage), раздел «Импорт стилей»).

View File

@@ -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)
├── infra/ # Техсервисы (theme, i18n, API-адаптеры)
├── ui/ # UI-кит без бизнес-логики
└── shared/ # Общие ресурсы (утилиты, типы, стили)
```
Принципы организации слоёв описаны в разделе [Архитектура](../basics/architecture/).
### Папка `app/`
Точка входа приложения и файловый роутинг Next.js (`layout.tsx`, `page.tsx`, route-сегменты).
`app/` подключает готовую инициализацию из нижних слоёв, но не реализует провайдеры, стили, UI-компоненты, хуки, сторы или сервисы.
Подробнее о границах слоя: [Архитектура → Слои → App](/docs/basics/architecture/layers#слой-app).
```text
src/app/
├── layout.tsx # Корневой layout
└── page.tsx # Главная страница
```
## Папка `.templates/`
Содержит шаблоны для генерации кода. Каждый подкаталог — шаблон отдельного типа модуля:
```text
.templates/
├── component/ # Шаблон компонента
├── screen/ # Шаблон экрана
├── layout/ # Шаблон layout
├── widget/ # Шаблон виджета
├── module/ # Шаблон бизнес-модуля
└── store/ # Шаблон стора
```
Подробнее о генерации описано в разделе [Шаблоны генерации](/docs/applied/templates/templates-intro).
## Конфигурационные файлы
| Файл | Назначение |
|---|---|
| `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_` доступны в клиентском коде. Остальные доступны только на сервере.

View File

@@ -0,0 +1,50 @@
---
title: REST-клиент
description: Настройка REST-клиента сервиса для работы с внешним API.
keywords: [rest, api, данные, infra, клиент, swr, стратегии]
---
# REST-клиент
Настройка REST-клиента сервиса для работы с внешним API.
## Настройка
Для каждого внешнего сервиса создаётся отдельный API-клиент: `pet-store-api`, `billing-api`, `maps-api`.
На этом этапе внешний API оформляется как модуль слоя `infra/`.
Клиент отвечает за:
- генерацию или ручное описание методов API;
- настройку `baseUrl`;
- заголовки и авторизацию;
- обработку ошибок;
- кастомизацию и расширение типов;
- GET-хуки для клиентских компонентов;
- прямое использование методов клиента в серверном коде и submit-функциях;
- публичный API модуля.
Если у API есть OpenAPI-спецификация — клиент генерируется автоматически. Если OpenAPI нет или он неполный — клиент создаётся вручную.
GET-хуки относятся к клиенту, потому что это прозрачные SWR-обёртки над GET-методами этого клиента.
Подробнее:
- [Настройка REST-клиента](/docs/applied/rest-client/setup/)
- [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto)
- [Ручное создание](/docs/applied/rest-client/setup/manual)
- [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks)
- [Использование REST-клиента](/docs/applied/rest-client/usage)
## Как читать раздел
Если API ещё не подключён — начните с [Настройки REST-клиента](/docs/applied/rest-client/setup/).
Если клиент уже создан и нужно вызвать его методы — откройте [Использование REST-клиента](/docs/applied/rest-client/usage).
Если клиент уже есть, но непонятно как получить данные — начните с раздела [Получение данных](/docs/applied/data-fetch/).
Если данные нужны в Client Component — сначала проверьте, есть ли [GET-хук REST-клиента](/docs/applied/rest-client/setup/hooks).
Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` — это уже не REST-клиент, а `business/`.

View File

@@ -0,0 +1,272 @@
---
title: Автогенерация REST-клиента
description: Генерация REST-клиента из OpenAPI-спецификации.
keywords: [rest, openapi, api-codegen, автогенерация, generated, npx]
---
# Автогенерация REST-клиента
Генерация REST-клиента из OpenAPI-спецификации.
## Когда использовать
Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки.
## Пример API
В примерах используется Swagger Petstore:
```text
https://petstore3.swagger.io/api/v3/openapi.json
```
Имена модуля:
```text
src/infra/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/infra/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/infra/pet-store-api/generated/
└── pet-store-api.generated.ts
```
Сгенерированный файл не правится руками и коммитится в репозиторий.
## Проверка методов
После генерации откройте `generated/pet-store-api.generated.ts` и проверьте фактические имена методов.
Для Petstore нужны GET-операции вида:
```ts
petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available })
petStoreApi.pet.getPetById({ petId: 10 })
```
Точные сигнатуры зависят от OpenAPI-схемы и версии генератора. В рабочих задачах всегда сверяйтесь с generated-файлом.
## Алгоритм для агента
После генерации агент должен действовать по шагам:
1. Открыть `generated/{service-name}.generated.ts`.
2. Найти фактические имена GET-методов клиента.
3. Для каждого нужного GET-метода найти generated-тип параметров и тип ответа.
4. Создать или обновить `client.ts` только для настройки транспорта и экспорта инстанса клиента.
5. Создать GET-хуки только для реально нужных GET-методов, не для всех методов API на всякий случай.
6. Для каждого GET-хука создать key-функцию формата `[serviceName, endpoint]`.
7. В key-функции вернуть `null`, если обязательные параметры не готовы.
8. В хуке принять `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
9. В fetcher вызвать generated-метод клиента с `params as GeneratedParams`.
10. Экспортировать хук и key-функцию из `hooks/index.ts`.
11. Экспортировать наружу только нужные generated-типы, generated enum, DTO и `hooks` через корневой `index.ts`.
Что агент не должен делать:
- Не использовать ключ `--swr` генератора.
- Не править `generated/*.generated.ts` руками.
- Не добавлять GET-хуки для POST, PUT, PATCH, DELETE.
- Не добавлять бизнес-флаги, тосты, редиректы и UI-состояние в GET-хук.
- Не создавать словари enum-маппинга внутри GET-хука.
- Не объявлять DTO и response-типы в файле хука.
- Не вызывать `useSWR` условно.
- Не добавлять `throw` в fetcher для неготовых params.
## `client.ts`
Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента.
```ts
// src/infra/pet-store-api/client.ts
import { Api, HttpClient } from './generated/pet-store-api.generated'
const baseUrl = process.env.NEXT_PUBLIC_PET_STORE_API_BASE_URL
if (!baseUrl) {
throw new Error('NEXT_PUBLIC_PET_STORE_API_BASE_URL is required')
}
const httpClient = new HttpClient({
baseUrl,
baseApiParams: {
secure: false,
headers: {
'Content-Type': 'application/json',
},
},
})
export const petStoreApi = new Api(httpClient)
```
Локальное значение `NEXT_PUBLIC_PET_STORE_API_BASE_URL` задаётся в `.env.local`. Не добавляйте fallback вроде `?? 'http://localhost:8080/api/v3'` или `?? ''`: если env-переменная не задана, клиент должен падать с явной ошибкой конфигурации.
`client.ts` не содержит расширения типов, `declare module` и `Extended`-типы. Он только настраивает транспорт и экспортирует инстанс клиента.
## GET-хуки
GET-хуки пишутся вручную после проверки generated-методов.
Пример для generated-метода `petStoreApi.pet.getPetById({ petId })`:
```ts
// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreApi } from '../client'
import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated'
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
if (!params?.petId) {
return null
}
return ['pet-store-api', `/pet/${params.petId}`] as const
}
/**
* Получает детальную карточку питомца с кешированием результата.
*/
export const useGetPetDetail = (
params?: GetPetByIdParams | null,
config?: SWRConfiguration<Pet>,
) => {
const key = getPetDetailKey(params)
const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams)
return useSWR<Pet>(key, fetcher, config)
}
```
Подробный контракт key-функций, `params`, `config` и запретов описан в разделе [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks).
## Расширение сгенерированных типов
Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
```text
src/infra/biocad-less-api/
├── generated/
│ └── biocad-less-api.generated.ts
├── types/
│ ├── term.ts
│ └── index.ts
├── client.ts
└── index.ts
```
Пример расширения generated-типа:
```ts
// src/infra/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<string, unknown>
}
```
```ts
// src/infra/biocad-less-api/types/index.ts
export type { TermRecordItemExtended } from './term'
```
`declare module` используется для добавления отсутствующих полей в generated-интерфейс. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файл.
## Публичный API
```ts
// src/infra/pet-store-api/index.ts
export { petStoreApi } from './client'
export type {
FindPetsByStatusParams,
GetPetByIdParams,
Pet,
} from './generated/pet-store-api.generated'
export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated'
export * from './hooks'
```
Наружу импортируют только из `infra/pet-store-api`, не из `generated/`.
Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`:
```ts
// src/infra/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` проверьте [использование REST-клиента](/docs/applied/rest-client/usage) или добавьте [GET-хук REST-клиента](/docs/applied/rest-client/setup/hooks) для Client Components.

View File

@@ -0,0 +1,313 @@
---
title: GET-хуки REST-клиента
description: Прозрачные SWR-обёртки над GET-методами REST-клиента.
keywords: [rest, swr, get-хуки, client components, infra]
---
# GET-хуки REST-клиента
Прозрачные SWR-обёртки над GET-методами REST-клиента.
## Зачем нужны
GET-хуки нужны, чтобы Client Components получали REST-данные через SWR, но не работали с `useSWR`, ключами кеша и fetcher напрямую.
## Где лежат
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
```text
src/infra/
└── pet-store-api/
├── client.ts
├── generated/
├── hooks/
│ ├── use-get-pet-list.hook.ts
│ ├── use-get-pet-detail.hook.ts
│ └── index.ts
├── types/
└── index.ts
```
## Контракт
- Один GET-хук = один GET-метод клиента.
- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`.
- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`.
- Хук принимает `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
- Для GET-метода без параметров хук принимает только `config?: SWRConfiguration<Data>`.
- Key-функция принимает те же `params`, что и хук.
- Key-функция возвращает `null`, если обязательные параметры не готовы.
- Проверка готовности запроса живёт в key-функции, а не в теле хука.
- Хук вызывает `useSWR` один раз и безусловно.
- Fetcher не проверяет `null`, не бросает ошибку и не вызывает метод клиента с `null`.
- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`.
- Хук возвращает тип ответа API: generated-тип или DTO из `types/`.
- Хук не объединяет несколько запросов.
- Хук не маппит DTO в доменную модель.
- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
## Формат SWR-ключа
SWR-ключ GET-хука всегда создаётся отдельной экспортируемой функцией.
Формат ключа:
```ts
['pet-store-api', '/pet/10'] as const
```
- Первый элемент — имя API-сервиса или REST-клиента в `kebab-case`.
- Второй элемент — endpoint запроса: path и query string.
- Key-функция возвращает `null`, когда запрос нельзя выполнять.
- Key-функция нужна и GET-хуку, и `SWRConfig fallback`.
- Не используйте произвольные части вроде `['pet-store-api', 'pet', 'detail', params]`.
- Не используйте только строку endpoint без имени сервиса.
Примеры ключей:
```ts
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
if (!params?.petId) {
return null
}
return ['pet-store-api', `/pet/${params.petId}`] as const
}
```
```ts
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
if (!params?.status) {
return null
}
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
}
```
```ts
export const getPetListByTagsKey = (params?: FindPetsByTagsParams | null) => {
if (!params?.tags.length) {
return null
}
return ['pet-store-api', `/pet/findByTags?tags=${params.tags.join(',')}`] as const
}
```
Если API допускает `0` как валидный идентификатор, не используйте проверку `!params?.id`. В таком случае проверяйте `null` и `undefined` явно.
## Пример списка
```ts
// src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreApi } from '../client'
import type {
FindPetsByStatusParams,
Pet,
} from '../generated/pet-store-api.generated'
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
if (!params?.status) {
return null
}
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
}
/**
* Получает список питомцев по статусу.
*/
export const useGetPetList = (
params?: FindPetsByStatusParams | null,
config?: SWRConfiguration<Pet[]>,
) => {
const key = getPetListKey(params)
const fetcher = () => petStoreApi.pet.findPetsByStatus(
params as FindPetsByStatusParams,
)
return useSWR<Pet[]>(key, fetcher, config)
}
```
`params as FindPetsByStatusParams` допустим только в fetcher: готовность параметров проверена в key-функции, а при `key = null` SWR не вызывает fetcher.
## Пример detail-запроса
```ts
// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreApi } from '../client'
import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated'
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
if (!params?.petId) {
return null
}
return ['pet-store-api', `/pet/${params.petId}`] as const
}
/**
* Получает детальную карточку питомца с кешированием результата.
*/
export const useGetPetDetail = (
params?: GetPetByIdParams | null,
config?: SWRConfiguration<Pet>,
) => {
const key = getPetDetailKey(params)
const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams)
return useSWR<Pet>(key, fetcher, config)
}
```
## Пример без параметров
```ts
// src/infra/pet-store-api/hooks/use-get-store-inventory.hook.ts
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreApi } from '../client'
import type { StoreInventory } from '../types'
export const getStoreInventoryKey = () => {
return ['pet-store-api', '/store/inventory'] as const
}
/**
* Получает инвентарь магазина.
*/
export const useGetStoreInventory = (
config?: SWRConfiguration<StoreInventory>,
) => {
return useSWR<StoreInventory>(
getStoreInventoryKey(),
() => petStoreApi.store.getInventory(),
config,
)
}
```
Если generated-метод возвращает безымянный тип вроде `Record<string, number>`, а тип нужен наружу, вынесите его в `types/`.
## Отложенный запрос
GET-хук может принимать `null` или `undefined` для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя.
```ts
const key = getPetDetailKey(params)
```
Если `params` не готов, key-функция вернёт `null`. SWR не вызовет fetcher для `null`-ключа.
Не добавляйте отдельные `isReady`, `throw new Error(...)` и условный вызов `useSWR`.
## Экспорт
```ts
// src/infra/pet-store-api/hooks/index.ts
export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
export {
getStoreInventoryKey,
useGetStoreInventory,
} from './use-get-store-inventory.hook'
```
```ts
// src/infra/pet-store-api/index.ts
export { petStoreApi } from './client'
export type {
FindPetsByStatusParams,
GetPetByIdParams,
Pet,
} from './generated/pet-store-api.generated'
export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated'
export * from './hooks'
export type { StoreInventory } from './types'
```
Наружу импортируют только из `infra/pet-store-api`, не из `generated/` и не из `hooks/` напрямую.
## Где заканчивается infra
```ts
// Хорошо: infra, прозрачный GET-хук
const { data: pets } = useGetPetList({ status: StatusEnum.Available })
```
```ts
// Хорошо: business, доменная интерпретация
export const useAvailablePets = () => {
const query = useGetPetList({ status: StatusEnum.Available })
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
```
`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
## Что запрещено
```ts
// Плохо — useSWR в компоненте
const { data } = useSWR(
['pet-store-api', '/pet/findByStatus?status=available'],
() => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }),
)
// Плохо — проверка готовности размазана по хуку
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
const key = params?.petId ? getPetDetailKey(params) : null
const fetcher = () => {
if (!params?.petId) {
throw new Error('Pet id is required')
}
return petStoreApi.pet.getPetById(params)
}
return useSWR<Pet>(key, fetcher)
}
// Плохо — условный вызов useSWR нарушает rules of hooks
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
const key = getPetDetailKey(params)
if (key === null) {
return useSWR(null, null)
}
return useSWR(key, () => petStoreApi.pet.getPetById(params))
}
// Плохо — несколько GET внутри infra-хука
export const usePetDashboard = () => {
const available = useGetPetList({ status: StatusEnum.Available })
const sold = useGetPetList({ status: StatusEnum.Sold })
return { available, sold }
}
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
const query = useSWR(...)
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
```
Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](/docs/applied/data-fetch/client-get-hook).

View File

@@ -0,0 +1,88 @@
---
title: Настройка REST-клиента
description: Подготовка REST-клиента сервиса к использованию.
keywords: [rest, клиент, infra, методы, openapi, get-хуки, swr]
---
# Настройка REST-клиента
Подготовка REST-клиента сервиса к использованию.
## Что настраиваем
REST-клиент — это infra-модуль, через который проект работает с внешним 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-хуки и бизнес-логика.
`baseUrl` API задаётся обязательной env-переменной без fallback-значения в коде. Не используйте записи вроде `process.env.NEXT_PUBLIC_PET_STORE_API_BASE_URL ?? 'http://localhost:8080/api/v3'` или `?? ''`: локальный URL должен лежать в `.env.local`, а отсутствие переменной должно приводить к явной ошибке конфигурации.
## Методы
Методы описывают конкретные запросы к API.
Они появляются одним из двух способов:
- генерируются из OpenAPI в `generated/`;
- создаются вручную в `methods/`.
Подробности:
- [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto)
- [Ручное создание](/docs/applied/rest-client/setup/manual)
## GET-хуки
Для GET-запросов добавляются GET-хуки REST-клиента.
Это прозрачные SWR-обёртки над GET-методами клиента. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components.
GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`.
Каждый GET-хук имеет экспортируемую key-функцию. SWR-ключ всегда имеет формат `[serviceName, endpoint]`: например `['pet-store-api', '/pet/10']`.
Хук принимает generated-параметры метода и SWR-настройки: `params?: GetPetByIdParams | null`, `config?: SWRConfiguration<Pet>`.
Подробности:
- [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks)
## Структура модуля
```text
src/infra/{service-name}/
├── client.ts # самописная оболочка и инстанс клиента
├── generated/ или methods/ # методы API
├── hooks/ # GET-хуки REST-клиента
├── types/ # DTO, именованные response-типы и расширения типов
├── errors/ # ошибки API, если нужны
└── index.ts # публичный API
```
`index.ts` — единственная точка входа в REST-модуль для внешнего кода.
Если generated-метод возвращает безымянный тип вроде `Record<string, number>`, а этот тип нужен снаружи, вынесите его в `types/`. Не объявляйте DTO внутри `hooks/use-get-*.hook.ts`.
## Что делаем дальше
1. Создайте методы клиента: [Автогенерация из OpenAPI](/docs/applied/rest-client/setup/auto) или [Ручное создание](/docs/applied/rest-client/setup/manual).
2. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks).
3. Проверьте прямые вызовы клиента: [Использование REST-клиента](/docs/applied/rest-client/usage).
4. После настройки клиента переходите к [Получению данных](/docs/applied/data-fetch/).

View File

@@ -0,0 +1,198 @@
---
title: Ручное создание REST-клиента
description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
keywords: [rest, ручной клиент, fetch, methods, dto, errors, infra]
---
# Ручное создание REST-клиента
Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
## Когда использовать
Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
Задача ручного клиента — дать такую же точку входа, как у автогенерированного клиента: именованный API-объект, методы по сущностям, DTO и GET-хуки рядом с клиентом.
## Что нужно создать
```text
src/infra/
└── 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/infra/pet-project-api/types/post.ts
export type PostDto = {
id: string
slug: string
title: string
}
export type PostListQueryDto = {
limit?: number
category?: string
}
```
```ts
// src/infra/pet-project-api/types/index.ts
export type { PostDto, PostListQueryDto } from './post'
```
Типы, которые нужны только базовому транспорту, можно держать отдельно:
```ts
// src/infra/pet-project-api/types/client.ts
export type QueryParams = Record<string, string | number | boolean>
```
## Ошибка API
Ошибка API тоже относится к REST-модулю.
```ts
// src/infra/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/infra/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<string, string> = {},
) {}
async get<T>(path: string, params: QueryParams = {}): Promise<T> {
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<T>
}
}
```
Это минимальный шаблон. Авторизация, дополнительные заголовки, `next.revalidate`, `post`, `formdata` и другие детали добавляются только когда они реально нужны API.
## Методы API
Методы группируются по сущностям в `methods/`. Они не знают про React, SWR и UI.
```ts
// src/infra/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<PostDto[]>('posts', query),
/** GET /posts/{slug} */
get: (slug: string) =>
client.get<PostDto>(`posts/${slug}`),
}
}
```
Метод возвращает DTO в форме API. Если данным нужен доменный смысл, маппинг делается выше, в `business/`.
## Публичный API
`index.ts` собирает именованный API-объект и открывает наружу только публичные части модуля.
```ts
// src/infra/pet-project-api/index.ts
import { PetProjectApiClient } from './client'
import { postsMethods } from './methods/posts'
const baseUrl = process.env.NEXT_PUBLIC_PET_PROJECT_API_BASE_URL
if (!baseUrl) {
throw new Error('NEXT_PUBLIC_PET_PROJECT_API_BASE_URL is required')
}
const client = new PetProjectApiClient(
baseUrl,
{ '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'
```
Внешний код импортирует только из `infra/pet-project-api`, не из внутренних файлов модуля.
## Правила
- `fetch` используется только внутри базового клиента.
- DTO запросов и ответов живут в `types/`.
- `client.ts` не содержит DTO, GET-хуки и бизнес-логику.
- `baseUrl` берётся из обязательной env-переменной без fallback-значения в коде.
- Методы лежат в `methods/` и возвращают DTO.
- GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components.
- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
Следующий шаг: [Использование REST-клиента](/docs/applied/rest-client/usage), [GET-хуки REST-клиента](/docs/applied/rest-client/setup/hooks) или [Получение данных](/docs/applied/data-fetch/).

View File

@@ -0,0 +1,21 @@
---
title: Использование REST-клиента
description: Как вызвать готовый REST-клиент в функции.
keywords: [rest, api client, submit, generated, pet-store-api]
---
# Использование REST-клиента
Как вызвать готовый REST-клиент в функции.
## Пример
```ts
import { petStoreApi } from 'infra/pet-store-api'
export const getPet = async (petId: number) => {
const pet = await petStoreApi.pet.getPetById({ petId })
console.log(pet)
}
```

View File

View File

@@ -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](/docs/applied/postcss)).
## Корневой `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 (
<html lang="ru">
<body>{children}</body>
</html>
)
}
```
`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](/docs/applied/postcss) — подключить процессор, чтобы заработали `@media (--md)` и вложенность.
- [Использование стилей](/docs/applied/styles/styles-usage) — правила написания CSS в компонентах.
- [SVG-спрайты](/docs/applied/svg-sprites/svg-sprites-setup) — стили иконок отдельно от глобальных.

View File

@@ -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.
- Исключение — нетривиальные хаки и обходные решения, к которым стоит оставить пояснение.

View File

@@ -0,0 +1,31 @@
---
title: SVG-спрайты
description: "Что такое SVG-спрайты и какие проблемы они решают."
---
# SVG-спрайты
Что такое SVG-спрайты и какие проблемы они решают.
## Проблема
Иконки в проекте — это десятки и сотни SVG-файлов, которые нужно как-то доставлять в интерфейс. Подход «один `<img>` на иконку» или инлайн SVG в каждом компоненте приводят к трём проблемам:
- **Дублирование.** Инлайн SVG в нескольких компонентах — один и тот же код размазан по проекту. Изменение иконки требует правок в десяти местах.
- **Размер бандла.** Каждый инлайн SVG — полный XML-код, который попадает в JS-бандл. Сотня иконок × средний размер SVG = сотни килобайт, которые браузер парсит как JavaScript, а не как статику.
- **Нет управления цветом.** Инлайн SVG жёстко закрашивает иконку. Сменить цвет по состоянию (`:hover`, `._disabled`) — значит дублировать SVG или городить `currentColor`-хаки в каждом компоненте.
## Решение
SVG-спрайты — это единый файл-контейнер, в который собираются все иконки проекта. В коде используется один React-компонент `<SvgSprite icon="name"/>`, а браузер загружает спрайт как статику — один раз, с кешированием.
Что дают SVG-спрайты:
- **Один источник.** Каждая иконка — один SVG-файл в `src/shared/sprites/`. Обновил файл — иконка обновилась везде.
- **Лёгкий бандл.** Спрайт отдаётся как статический файл из `public/`, не попадает в JavaScript. Типы имён иконок генерируются автоматически — автодополнение работает без ручных описаний.
- **Цвет через CSS.** При сборке цвета в SVG заменяются на CSS-переменные. Цвет иконки меняется через `color` родителя или через переменные `--icon-color-N` — как любой другой стиль.
## Состав раздела
- [Настройка](/docs/applied/svg-sprites/svg-sprites-setup) — подключение пакета, конфигурация, первая генерация.
- [Использование](/docs/applied/svg-sprites/svg-sprites-usage) — добавление иконок, компонент `<SvgSprite/>`, управление цветом.

View File

@@ -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/<group>/` — это слой `shared` SLM-архитектуры (см. [Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)). В `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. Глобальный спрайт (иконки) подключается через `<link rel="preload">` в корневом 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 (
<html lang="ru">
<head>
<link rel="preload" href="/sprites/icons.sprite.stack.svg" as="image" />
</head>
<body>{children}</body>
</html>
)
}
```
Локальные спрайты (если есть) подключаются аналогично в 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/` из архитектуры проекта (→ [Архитектура](/docs/basics/architecture/)) |
| `sprites[0].name` | `icons` | Основной спрайт всегда называется `icons` |
### Трансформации
Все значения по умолчанию оставлять включёнными:
```ts
transform: {
removeSize: true,
replaceColors: true,
addTransition: true,
}
```
Явно прописывать блок `transform` не нужно — пакет применяет эти значения по умолчанию.
Отключать `replaceColors` — только для отдельного спрайта с фиксированной палитрой (например, брендовые логотипы). Делать это на уровне спрайта, не глобально.
### Режим
По умолчанию `mode: 'stack'` — не указывать явно. Переход на `symbol` требует обоснования: превью и примеры в пакете оптимизированы под `stack`.
## Дальше
- [Использование](/docs/applied/svg-sprites/svg-sprites-usage) — добавление иконок, компонент `<SvgSprite/>`, управление цветом.

View File

@@ -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. **Импортировать компонент.** Компонент `<SvgSprite/>` генерируется пакетом вместе с типами имён иконок — автодополнение работает без ручных описаний:
```tsx
import { SvgSprite } from 'ui/svg-sprite'
<SvgSprite icon="new-icon" />
```
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);
}
```

View File

@@ -0,0 +1,97 @@
---
title: Создание шаблонов генерации
description: "Структура шаблонов, синтаксис переменных и примеры."
keywords: [шаблоны, templates, .templates, syntax, переменные, kebabCase, pascalCase, scaffold]
---
<!-- @formatter:off -->
::: v-pre
# Создание шаблонов генерации
Структура шаблонов, синтаксис переменных и примеры.
## Структура шаблонов
Все шаблоны лежат в `.templates/` в корне проекта. Каждая папка — отдельный шаблон.
```text
.templates/
├── component/ # шаблон компонента
│ └── {{name.kebabCase}}/
│ ├── styles/
│ │ └── {{name.kebabCase}}.module.css
│ ├── types/
│ │ └── {{name.kebabCase}}-props.type.ts
│ ├── {{name.kebabCase}}.tsx
│ └── index.ts
└── store/ # шаблон Zustand стора
└── {{name.kebabCase}}/
├── {{name.kebabCase}}.store.ts
├── {{name.kebabCase}}.type.ts
└── index.ts
```
## Обязательный шаблон компонента
Перед созданием компонентов в проекте должен существовать шаблон `.templates/component`.
Если шаблона нет, компонент не создаётся вручную. Сначала создаётся шаблон компонента, затем компонент генерируется через [VS Code или CLI](/docs/applied/templates/templates-usage).
## Синтаксис шаблонов
### Переменные
Переменные работают в именах файлов/папок и внутри файлов:
```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'
```
## Дальше
- [Использование](/docs/applied/templates/templates-usage) — генерация через VS Code плагин и CLI.
:::

View File

@@ -0,0 +1,32 @@
---
title: Шаблоны генерации
description: "Что такое шаблоны кодогенерации и какие проблемы они решают."
---
# Шаблоны генерации
Что такое шаблоны кодогенерации и какие проблемы они решают.
## Проблема
Каждый новый модуль в проекте — компонент, стор, бизнес-модуль — требует однотипной структуры файлов и boilerplate-кода. Ручное создание приводит к трём проблемам:
- **Расхождения.** Разные разработчики создают модули по-разному: забывают `index.ts`, называют типы не по канону, пропускают стили.
- **Время.** Создание одного компонента с типами, стилями и экспортом — 510 минут рутины. За спринт набегают часы.
- **Ошибки копипасты.** Копирование существующего модуля и переименование — источник опечаток и забытых ссылок.
## Решение
Шаблоны кодогенерации — это папки с файлами-заготовками в `.templates/`. Вместо ручного создания файлов разработчик вызывает генератор, указывает имя — и получает готовый модуль со всей структурой, именами и boilerplate, подставленными автоматически.
Что дают шаблоны:
- **Единообразие.** Все модули одного типа идентичны по структуре. Канон живёт в шаблоне, а не в памяти разработчика.
- **Скорость.** Генерация модуля — одна команда. Остальное время — на бизнес-логику.
- **Согласованность с архитектурой.** Шаблоны учитывают SLM: правильные слои, сегменты, экспорты. Отклонение от стайлгайда требует осознанного усилия, а не случайного упущения.
## Состав раздела
- [Настройка](/docs/applied/templates/templates-setup) — первичная установка: скачивание стандартного набора шаблонов в проект.
- [Создание шаблонов](/docs/applied/templates/templates-create) — структура файлов, синтаксис переменных, примеры.
- [Использование](/docs/applied/templates/templates-usage) — генерация через VS Code плагин и CLI.

View File

@@ -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 ...` отрабатывает без ошибок.
## Дальше
- [Создание шаблонов](/docs/applied/templates/templates-create) — структура файлов, синтаксис переменных, примеры.
- [Использование](/docs/applied/templates/templates-usage) — генерация через VS Code плагин и CLI.

View File

@@ -0,0 +1,45 @@
---
title: Использование шаблонов генерации
description: Генерация файлов из шаблонов через VS Code плагин и CLI.
keywords: [шаблоны, templates, generate, VS Code, CLI, gromlab/create, npx, scaffold]
---
# Использование шаблонов генерации
Генерация файлов из шаблонов через VS Code плагин и CLI.
::: danger Ручное создание запрещено
Файлы, для которых есть шаблоны в `.templates/`, создаются только генератором. Ручное создание компонента, модуля, стора или другого шаблонного блока запрещено.
Если нужного шаблона нет, сначала создайте шаблон в `.templates/`, затем сгенерируйте код на его основе.
:::
## Через 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` отдельно не добавляется.

View File

@@ -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` с общими для команды настройками.

View File

@@ -0,0 +1,109 @@
---
title: SLM Design
description: Назначение архитектуры, ключевые принципы и карта разделов документации
---
# SLM Design
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
## Разделы спецификации
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты.
## Преимущества
### Вертикальная организация домена
Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы.
### Dependency Injection без фреймворков
Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий.
### Разделение ответственности без перегрузки слоёв
Сервисы приложения (`infra/`), 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/
├── infra/
│ ├── theme/
│ ├── i18n/
│ ├── backend-api/
│ └── logger/
├── ui/
│ ├── button/
│ ├── input/
│ ├── modal/
│ ├── toast/
│ └── dropdown/
└── shared/
├── lib/
├── types/
└── styles/
```
## Принципы
- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле.
- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости.
- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API.
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.

View File

@@ -0,0 +1,254 @@
---
title: Слои
description: Иерархия слоёв от app до shared, правила зависимостей и зона ответственности каждого слоя
---
# Слои
Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом.
## Определение
**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.**
## Группы слоёв
Слои делятся на три группы:
| Группа | Слои | Описание |
|--------|------|----------|
| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы |
| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит |
| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги |
## Направление зависимостей
Любой импорт между модулями — только через публичный API.
```
app → [ layouts | screens ] → widgets → business → infra → ui → shared
```
- `layouts` и `screens` — параллельные слои, не импортируют друг друга
- Модули одного слоя в группе «Композиция» изолированы друг от друга
- Модули одного слоя `infra` и `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 и сервисами.
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `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/
```
### Требования
- Один модуль = один бизнес-домен
- Циклические зависимости между доменами запрещены
- Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты
- Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую
- Доменные типы (`User`, `Product`) живут здесь, не в `shared/`
## Слой infra
Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль.
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`.
Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
```text
src/infra/
├── theme/
├── i18n/
├── backend-api/
├── maps-api/
├── logger/
├── feature-flags/
└── realtime/
```
### Требования
- Один модуль = один техсервис
- Импортирует `infra/`, `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
Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене.
Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует.
Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь.
```text
src/shared/
├── lib/
├── types/
├── styles/
└── sprites/
```
### Требования
- Не имеет runtime-состояния

View File

@@ -0,0 +1,289 @@
---
title: Модули
description: Структура модуля, типы (UI, бизнес, инфра), публичный API, отличие модуля от компонента
---
# Модули
Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом.
## Определение
**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.**
Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно.
Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность.
Главная граница модуля — не папка, а ответственность.
## Компонент
**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.**
Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля.
> Компонент отображает. Модуль организует.
Компонент не может:
- Импортировать код проекта за пределами родительского модуля.
- Владеть архитектурными зависимостями.
- Содержать любые компоненты.
- Содержать любые модули.
- Делать внешние запросы.
- Самостоятельно получать данные.
- Выбирать источник данных.
- Композировать данные.
- Вызывать сценарные хуки.
- Оркестрировать сценарий.
- Композировать модули.
- Решать, как устроен процесс.
- Содержать бизнес-логику.
- Содержать сценарную логику.
Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль.
```text
auth/
├── ui/
│ └── logout-button/
│ ├── logout-button.tsx
│ ├── styles/
│ │ └── logout-button.module.css
│ ├── types/
│ │ └── logout-button-props.type.ts
│ └── index.ts
└── index.ts
```
## Что считается модулем
Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу.
Примеры модулей:
- `screens/home/` — модуль страницы.
- `widgets/page-heading/` — модуль виджета.
- `business/auth/` — модуль бизнес-домена.
- `infra/theme/` — модуль инфраструктурного сервиса.
- `ui/button/` — модуль UI-kit сущности.
- `screens/home/parts/hero-section/` — вложенный модуль страницы.
Не считаются модулями:
- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты.
- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`.
- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента.
## Типы модулей
Тип модуля определяет обязательный корневой файл и стартовую структуру.
### UI-модуль
Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне:
```text
header/
├── header.tsx
└── index.ts
```
`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу.
### Бизнес-модуль
Бизнес-модуль — модуль, который строится вокруг публичного runtime API.
Бизнес-модуль обязан иметь фабрику в корне:
```text
auth/
├── auth.factory.ts
├── index.ts
└── types/
```
Фабрика возвращает публичный runtime API модуля.
### Инфраструктурный модуль
Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции.
Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса.
```text
theme/
├── index.ts
├── config/
├── hooks/
├── styles/
└── ui/
```
```text
backend-api/
├── backend-api.client.ts
├── config/
├── types/
└── index.ts
```
## Структура
Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности.
```text
{module-name}/
├── {module-name}.factory.ts # фабрика (для business-модулей)
├── {module-name}.tsx # корневой файл модуля (опционален)
├── ui/ # компоненты модуля
├── parts/ # вложенные модули
├── hooks/ # хуки
├── stores/ # сторы состояния
├── services/ # внешние источники данных
├── mappers/ # трансформация данных между форматами
├── types/ # типы
├── styles/ # стили
├── lib/ # утилиты модуля
├── config/ # константы и конфигурация
└── index.ts # публичный API
```
Подробное описание сегментов — в разделе [Сегменты](./segments.md).
## Публичный API
Внешний код импортирует модуль только через публичный API.
```ts
// Хорошо
import { customerFactory } from '@/business/customer'
import type { Customer } from '@/business/customer'
```
```ts
// Плохо
import { validateToken } from '@/business/auth/lib/tokens'
```
`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи.
Внутренние сегменты модуля остаются деталями реализации.
Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика.
```ts
// business/customer/index.ts
export { customerFactory } from './customer.factory'
export type { Customer } from './types/customer.type'
export type { CustomerApi } from './types/customer-api.type'
export type { CustomerDeps } from './types/customer-deps.type'
export type { CustomerFactory } from './types/customer-factory.type'
```
## Фабрика
Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля.
Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика.
Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type``import type` не является runtime-зависимостью.
Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция».
### Структура business-модуля
```text
business/customer/
├── customer.factory.ts
├── index.ts
└── types/
├── customer.type.ts
├── customer-api.type.ts
├── customer-deps.type.ts
└── customer-factory.type.ts
```
### Типы
```ts
// business/customer/types/customer-api.type.ts
export type CustomerApi = {
useCustomer: () => Customer
CustomerCard: (props: CustomerCardProps) => ReactNode
}
```
```ts
// business/order/types/order-deps.type.ts
export type OrderDeps = {
customer: Pick<CustomerApi, 'useCustomer'>
}
```
```ts
// business/order/types/order-factory.type.ts
export type OrderFactory = (deps: OrderDeps) => OrderApi
```
### Фабрика без зависимостей
```ts
// business/customer/customer.factory.ts
import type { CustomerFactory } from './types/customer-factory.type'
export const customerFactory: CustomerFactory = () => {
return {
useCustomer,
CustomerCard,
}
}
```
### Фабрика с зависимостями
```ts
// business/order/order.factory.ts
import type { OrderFactory } from './types/order-factory.type'
export const orderFactory: OrderFactory = (deps) => {
return {
useOrder,
OrderCard,
}
}
```
### Композиция на уровне screen
```tsx
// screens/home/home.screen.tsx
import { customerFactory } from '@/business/customer'
import { orderFactory } from '@/business/order'
const customer = customerFactory()
const order = orderFactory({ customer })
const { useOrder, OrderCard } = order
export const HomeScreen = () => {
const currentOrder = useOrder()
return <OrderCard order={currentOrder} />
}
```
## Жизненный цикл
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
- Нужен на одной странице → `screens/{name}/parts/`
- Появился в 2+ местах → поднимается по природе:
- абстрактный UI → `ui/`
- блок с данными/логикой → `widgets/`
- представление бизнес-домена → `business/{area}/parts/`
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.

View File

@@ -0,0 +1,181 @@
---
title: Сегменты
description: Сегменты внутри модуля (ui/, model/, lib/ и др.), назначение и правила размещения файлов
---
# Сегменты
Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит.
## Определение
**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.**
## Обзор
| Сегмент | Содержимое |
|---------|------------|
| `ui/` | Презентационные компоненты родительского модуля |
| `parts/` | Вложенные модули со своими сегментами |
| `hooks/` | React-хуки |
| `stores/` | Сторы состояния |
| `services/` | Работа с внешними источниками данных |
| `mappers/` | Трансформация данных между форматами |
| `types/` | TypeScript-типы и интерфейсы |
| `styles/` | Стили |
| `lib/` | Утилиты и хелперы модуля |
| `config/` | Константы и конфигурация |
## Сегмент ui/
Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля.
Компонент в `ui/`:
- Находится в собственной папке.
- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`.
- Не содержит любые компоненты.
- Не содержит любые модули.
- Не импортирует код проекта за пределами родительского модуля.
- Не делает внешние запросы.
- Не вызывает сценарные хуки.
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
- Не содержит бизнес-логику или сценарную логику.
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент).
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
```text
user/
├── ui/
│ ├── user-avatar/
│ │ ├── user-avatar.tsx
│ │ ├── styles/
│ │ │ └── user-avatar.module.css
│ │ ├── types/
│ │ │ └── user-avatar-props.type.ts
│ │ └── index.ts
│ └── user-status/
│ ├── user-status.tsx
│ └── index.ts
├── types/
├── hooks/
├── user.tsx
└── index.ts
```
Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `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/` — компонент родительского модуля без собственной архитектурной ответственности.
Вложенность `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
```

View File

@@ -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
<input type="text" placeholder="Введите имя" />
```
**Плохо**
```ts
// Плохо: двойные кавычки в TS и конкатенация вместо шаблонной строки.
const label = "Сохранить";
const title = 'Привет, ' + name;
```
```tsx
// Плохо: одинарные кавычки в JSX-атрибутах.
<input type='text' placeholder='Введите имя' />
```
## Точки с запятой и запятые
- Допускаются упущения точки с запятой, если код остаётся читаемым и однозначным.
- В многострочных массивах, объектах и параметрах функции запятая в конце допускается, но не обязательна.
## Импорты
- В именованных импортах использовать пробелы внутри фигурных скобок.
- Типы импортировать через `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 } };
```

View File

@@ -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;
}
```

View File

@@ -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.<suffix>.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<string, User>;
const userIds = ['u1', 'u2'];
const ordersMap = new Map<string, Order>();
const featureFlagsDict = { beta: true, legacy: false } as Record<string, boolean>;
```
**Плохо**
```ts
// Плохо: имя не отражает, что это коллекция.
const user = [];
// Плохо: словарь назван как массив.
const usersMap = [];
// Плохо: по имени непонятно, что это словарь.
const users = {} as Record<string, User>;
```

View File

@@ -0,0 +1,42 @@
---
title: Технологии и библиотеки
description: Какие библиотеки и инструменты используются в проекте.
---
# Технологии и библиотеки
Какие библиотеки и инструменты используются в проекте.
## Что используем
### Стек
- `React` / `TypeScript` — основной стек для UI и приложения.
- `Next.js` — для продуктовых сайтов.
### Архитектура
- `SLM Design` — собственная модульная архитектура проекта. Подробнее в разделе [Архитектура](/docs/basics/architecture/).
### 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` — шаблонизатор для создания слоёв и других файлов из шаблонов.

View File

@@ -0,0 +1,62 @@
---
title: Типизация
description: Как типизируется код в проекте.
---
# Типизация
Как типизируется код в проекте.
## Общие правила
- Указывать типы для параметров компонентов и параметров функций.
- Предпочитать `type` для описания сущностей и `interface` для расширяемых контрактов.
- Избегать `any` и `unknown` без необходимости.
- Не использовать `ts-ignore`, кроме крайних случаев с явным комментарием причины.
## React-компоненты
- Пропсы компонента типизировать через отдельный `Props`.
- Возвращаемый тип компонента не указывать: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата.
## Функции
- Для публичных функций указывать возвращаемый тип.
- Не полагаться на неявный вывод для важных 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;
```

View File

@@ -0,0 +1,79 @@
---
title: NextJS Style Guide
description: Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
---
# NextJS Style Guide
Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
## Использование
**Для AI-агентов:**
- [llms.txt](/llms.txt) — Карта разделов, оглавление со ссылками на разделы.
- [llms-full.txt](/llms-full.txt) — Вся документация одним файлом.
**Для проекта:**
- [nextjs-style-guide.zip](/nextjs-style-guide.zip) — Набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
## Структура документации
### Подсказки
[Подсказки](/docs/workflow) — короткие ответы на типовые вопросы и решения для спорных ситуаций.
### Базовые правила
**Каким должен быть код** — стандарты, не привязанные к конкретной технологии.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Технологии и библиотеки | Какой стек используем? |
| Именование | Как называть файлы, переменные, компоненты, хуки? |
| SLM Design | Что такое SLM и зачем она нужна? |
| Архитектура: Слои | Какие слои есть и как между ними устроены зависимости? |
| Архитектура: Модули | Что такое модуль и как он устроен? |
| Архитектура: Сегменты | Какие сегменты есть внутри модуля? |
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
| Документирование | Как писать JSDoc: что документировать, а что нет? |
| Типизация | Как типизировать: type vs interface, any/unknown? |
### Настройка
**Как сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Создание проекта из шаблона | Как начать проект из готового шаблона? |
| Создание проекта вручную | Как поднять проект с нуля без шаблона? |
| Чистая установка Next.js | Как поставить голый Next.js под дальнейшую сборку? |
| Алиасы импортов | Как настроить алиасы импортов? |
| Biome | Как настроить линтер и форматтер? |
| PostCSS | Какие плагины PostCSS нужны и как их настроить? |
| Стили | Как подключить базовые стили и токены? |
| SVG-спрайты | Как подключить генерацию SVG-спрайтов? |
| Шаблоны генерации | Как подключить шаблоны для кодогенерации? |
| VS Code | Как настроить редактор для проекта? |
### Использование
**Как это устроено и как этим пользоваться** — структура, примеры и правила для конкретных областей.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Структура проекта | Как организованы папки и файлы по SLM? |
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
| Файлы роутинга | Как описывать layout, page, loading, error, not-found? |
| REST-клиент | Как настроить клиент внешнего REST API? |
| Получение данных | Как выбрать способ получения данных под рендер страницы? |
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
| Стили | Как писать CSS: вложенность, медиа, токены? |
| SVG-спрайты | Как использовать SVG-спрайты в коде? |
| Изображения | _(не заполнен)_ |
| Видео | _(не заполнен)_ |
| Stores | _(не заполнен)_ |
| Хуки | _(не заполнен)_ |
| Шрифты | _(не заполнен)_ |
| Локализация | _(не заполнен)_ |

View File

@@ -0,0 +1,2 @@
v0.1.5
2026-05-11T18:29:26.821Z

View File

@@ -0,0 +1,114 @@
---
title: SLM Design
description: "Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили."
---
# SLM Design
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
## Разделы спецификации
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
- [Монорепозитории](./monorepo.md) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business.
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории.
## Преимущества
### Вертикальная организация домена
Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы.
### Dependency Injection без фреймворков
Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий.
### Разделение ответственности без перегрузки слоёв
Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода.
### Горизонтальная инкапсуляция
Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга.
### Колокация по умолчанию
Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями.
### Явное разделение каркаса и контента
Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью.
### Масштабирование через группировку
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции).
### Адаптация к монорепозиториям
SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы.
## Происхождение
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/
├── infra/
│ ├── theme/
│ ├── i18n/
│ ├── backend-api/
│ └── logger/
├── ui/
│ ├── button/
│ ├── input/
│ ├── modal/
│ ├── toast/
│ └── dropdown/
└── shared/
├── lib/
├── types/
└── styles/
```
## Принципы
- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле.
- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости.
- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API.
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.

View File

@@ -0,0 +1,254 @@
---
title: Слои
description: "Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом."
---
# Слои
Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом.
## Определение
**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.**
## Группы слоёв
Слои делятся на три группы:
| Группа | Слои | Описание |
|--------|------|----------|
| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы |
| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит |
| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги |
## Направление зависимостей
Любой импорт между модулями — только через публичный API.
```
app → [ layouts | screens ] → widgets → business → infra → ui → shared
```
- `layouts` и `screens` — параллельные слои, не импортируют друг друга
- Модули одного слоя в группе «Композиция» изолированы друг от друга
- Модули одного слоя `infra` и `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 и сервисами.
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `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/
```
### Требования
- Один модуль = один бизнес-домен
- Циклические зависимости между доменами запрещены
- Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты
- Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую
- Доменные типы (`User`, `Product`) живут здесь, не в `shared/`
## Слой infra
Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль.
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`.
Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
```text
src/infra/
├── theme/
├── i18n/
├── backend-api/
├── maps-api/
├── logger/
├── feature-flags/
└── realtime/
```
### Требования
- Один модуль = один техсервис
- Импортирует `infra/`, `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
Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене.
Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует.
Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь.
```text
src/shared/
├── lib/
├── types/
├── styles/
└── sprites/
```
### Требования
- Не имеет runtime-состояния

View File

@@ -0,0 +1,213 @@
---
title: Модули
description: "Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом."
---
# Модули
Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом.
## Определение
**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.**
Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно.
Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность.
Главная граница модуля — не папка, а ответственность.
## Компонент
**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.**
Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля.
> Компонент отображает. Модуль организует.
Компонент не может:
- Импортировать код проекта за пределами родительского модуля.
- Владеть архитектурными зависимостями.
- Содержать любые компоненты.
- Содержать любые модули.
- Делать внешние запросы.
- Самостоятельно получать данные.
- Выбирать источник данных.
- Композировать данные.
- Вызывать сценарные хуки.
- Оркестрировать сценарий.
- Композировать модули.
- Решать, как устроен процесс.
- Содержать бизнес-логику.
- Содержать сценарную логику.
Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль.
```text
auth/
├── ui/
│ └── logout-button/
│ ├── logout-button.tsx
│ ├── styles/
│ │ └── logout-button.module.css
│ ├── types/
│ │ └── logout-button-props.type.ts
│ └── index.ts
└── index.ts
```
## Что считается модулем
Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу.
Примеры модулей:
- `screens/home/` — модуль страницы.
- `widgets/page-heading/` — модуль виджета.
- `business/auth/` — модуль бизнес-домена.
- `infra/theme/` — модуль инфраструктурного сервиса.
- `ui/button/` — модуль UI-kit сущности.
- `screens/home/parts/hero-section/` — вложенный модуль страницы.
Не считаются модулями:
- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты.
- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`.
- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента.
## Типы модулей
Тип модуля определяет обязательный корневой файл и стартовую структуру.
### UI-модуль
Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне:
```text
header/
├── header.tsx
└── index.ts
```
`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу.
### Бизнес-модуль
Бизнес-модуль — модуль, который строится вокруг публичного runtime API.
Бизнес-модуль обязан иметь фабрику в корне:
```text
auth/
├── auth.factory.ts
├── index.ts
└── types/
```
Фабрика возвращает публичный runtime API модуля.
### Инфраструктурный модуль
Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции.
Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса.
```text
theme/
├── index.ts
├── config/
├── hooks/
├── styles/
└── ui/
```
```text
backend-api/
├── backend-api.client.ts
├── config/
├── types/
└── index.ts
```
## Структура
Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности.
```text
{module-name}/
├── {module-name}.factory.ts # фабрика (для business-модулей)
├── {module-name}.tsx # корневой файл модуля (опционален)
├── ui/ # компоненты модуля
├── parts/ # вложенные модули
├── hooks/ # хуки
├── stores/ # сторы состояния
├── services/ # внешние источники данных
├── mappers/ # трансформация данных между форматами
├── types/ # типы
├── styles/ # стили
├── lib/ # утилиты модуля
├── config/ # константы и конфигурация
└── index.ts # публичный API
```
Подробное описание сегментов — в разделе [Сегменты](./segments.md).
## Публичный API
Внешний код импортирует модуль только через публичный API.
```ts
// Хорошо
import { customerFactory } from '@/business/customer'
import type { Customer } from '@/business/customer'
```
```ts
// Плохо
import { validateToken } from '@/business/auth/lib/tokens'
```
`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи.
Внутренние сегменты модуля остаются деталями реализации.
Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика.
```ts
// business/customer/index.ts
export { customerFactory } from './customer.factory'
export type { Customer } from './types/customer.type'
export type { CustomerApi } from './types/customer-api.type'
export type { CustomerDeps } from './types/customer-deps.type'
export type { CustomerFactory } from './types/customer-factory.type'
```
## Фабрика
Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля.
Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика.
Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type``import type` не является runtime-зависимостью.
Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция».
### Примеры
Пример реализации фабрики в React см. в [Создание фабрики](../examples/react/factory.md).
Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](../examples/react/factory-composition.md).
## Жизненный цикл
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
- Нужен на одной странице → `screens/{name}/parts/`
- Появился в 2+ местах → поднимается по природе:
- абстрактный UI → `ui/`
- блок с данными/логикой → `widgets/`
- представление бизнес-домена → `business/{area}/parts/`
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.

View File

@@ -0,0 +1,235 @@
---
title: Монорепозитории
description: "Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов."
---
# Монорепозитории
Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов.
## Определение
**Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.**
## Базовая структура
Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`.
```text
repo/
├── apps/
│ ├── web/
│ │ └── src/
│ │ ├── app/
│ │ ├── layouts/
│ │ ├── screens/
│ │ ├── widgets/
│ │ ├── business/
│ │ ├── infra/
│ │ ├── ui/
│ │ └── shared/
│ └── admin/
│ └── src/
│ └── ...
└── packages/
├── ui/
│ ├── button/ # самостоятельный пакет UI-модуля
│ ├── input/ # самостоятельный пакет UI-модуля
│ └── modal/ # самостоятельный пакет UI-модуля
├── infra/
│ ├── theme/ # самостоятельный пакет infra-модуля
│ ├── backend-api/ # самостоятельный пакет infra-модуля
│ └── logger/ # самостоятельный пакет infra-модуля
└── shared/ # единый shared-пакет
├── package.json
└── src/
├── lib/ # переиспользуемые утилиты
├── helpers/ # переиспользуемые helpers
└── index.ts
```
`apps/{app}/src` — граница SLM-приложения. `packages/*` находятся выше SLM и не добавляют новые архитектурные слои.
## Группировка frontend-пакетов
Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: `ui`, `infra`, `shared`.
```text
packages/ui/* # пакеты UI-модулей
packages/infra/* # пакеты infra-модулей
packages/shared # единый shared-пакет
```
Эта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри `packages/`. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM.
## Пакет и модуль
Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации.
В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. `packages/shared` устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению.
```text
packages/ui/button/
packages/ui/modal/
packages/infra/theme/
packages/infra/backend-api/
packages/shared/
```
## Что остаётся в приложении
Слои `app`, `layouts`, `screens`, `widgets` и `business` остаются внутри конкретного приложения.
```text
apps/web/src/app/
apps/web/src/layouts/
apps/web/src/screens/
apps/web/src/widgets/
apps/web/src/business/
```
`app`, `layouts` и `screens` привязаны к роутингу, каркасу и страницам конкретного приложения. `widgets` не выносятся в пакеты, потому что это слой композиции интерфейса приложения.
`business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой.
## Что можно выносить
В пакеты выносится только код из `ui`, `infra` и `shared`, который потенциально будет использоваться в двух и более фронтенд-приложениях монорепозитория.
| Группа | Что выносить | Пример |
|--------|--------------|--------|
| `packages/ui/*` | Самостоятельные UI-модули без бизнес-логики | `packages/ui/button` |
| `packages/infra/*` | Самостоятельные технические сервисы | `packages/infra/backend-api` |
| `packages/shared` | Общие утилиты, helpers и фундаментальный код | `packages/shared` |
Пакет можно создавать сразу, если модуль имеет общую природу и ожидается его переиспользование между приложениями. App-specific код остаётся внутри приложения.
## UI-пакеты
В `packages/ui/*` размещаются переиспользуемые UI-модули.
```text
packages/ui/button/
├── package.json
└── src/
├── button.tsx
├── styles/
├── types/
└── index.ts
```
UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц.
## Infra-пакеты
В `packages/infra/*` размещаются переиспользуемые инфраструктурные модули.
```text
packages/infra/backend-api/
├── package.json
└── src/
├── clients/
├── config/
├── types/
└── index.ts
```
Привязанные к конкретному приложению сервисы остаются в `apps/{app}/src/infra`. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок.
## Shared-пакет
`packages/shared` является единым пакетом.
```text
packages/shared/
├── package.json
└── src/
├── lib/
├── helpers/
└── index.ts
```
В `packages/shared` сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте.
Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий `shared` не выносятся.
## Имена пакетов и импорты
Путь импорта задаётся `name` в `package.json`, а не расположением директории.
```json
{
"name": "@repo/theme"
}
```
```text
packages/infra/theme/package.json
```
```ts
import { ThemeProvider } from '@repo/theme'
```
Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены.
```ts
// Хорошо
import { Button } from '@repo/button'
// Плохо
import { Button } from '@repo/button/src/button'
```
## Зависимости
На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений.
```text
apps → packages
packages -/→ apps
```
Внутри приложения продолжает действовать обычное направление зависимостей SLM.
```text
app → [ layouts | screens ] → widgets → business → infra → ui → shared
```
Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях.
## Когда не выносить
Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API.
Фактическое использование в одном приложении не запрещает пакет, если модуль имеет общую природу и потенциально нужен нескольким приложениям.
```text
# Плохо
apps/web/src/screens/home/parts/promo-section/
packages/ui/promo-section/
```
Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным `parts/`-модулем.
## Конфигурационные пакеты
Конфигурационные пакеты не относятся к SLM-архитектуре.
Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в `packages/`, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри `src/`.
## Правила
- SLM применяется внутри каждого `apps/{app}/src`.
- Frontend-пакеты, вынесенные из SLM-приложений, группируются в `packages/ui`, `packages/infra`, `packages/shared`.
- Группы `packages/ui`, `packages/infra`, `packages/shared` не являются SLM-слоями.
- В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей.
- В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей.
- `packages/shared` является единым пакетом для переиспользуемых утилит и helpers.
- Модуль можно размещать в пакете, если он потенциально будет использоваться в двух и более фронтенд-приложениях.
- `business`, `app`, `layouts`, `screens`, `widgets` не выносятся в пакеты.
- Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`.
- Пакеты не импортируют приложения.
- Межпакетные импорты идут только через публичный API.
- Deep imports внутрь пакетов запрещены.
- Локальная колокация важнее преждевременного выноса в `packages/*`.

View File

@@ -0,0 +1,181 @@
---
title: Сегменты
description: "Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит."
---
# Сегменты
Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит.
## Определение
**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.**
## Обзор
| Сегмент | Содержимое |
|---------|------------|
| `ui/` | Презентационные компоненты родительского модуля |
| `parts/` | Вложенные модули со своими сегментами |
| `hooks/` | React-хуки |
| `stores/` | Сторы состояния |
| `services/` | Работа с внешними источниками данных |
| `mappers/` | Трансформация данных между форматами |
| `types/` | TypeScript-типы и интерфейсы |
| `styles/` | Стили |
| `lib/` | Утилиты и хелперы модуля |
| `config/` | Константы и конфигурация |
## Сегмент ui/
Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля.
Компонент в `ui/`:
- Находится в собственной папке.
- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`.
- Не содержит любые компоненты.
- Не содержит любые модули.
- Не импортирует код проекта за пределами родительского модуля.
- Не делает внешние запросы.
- Не вызывает сценарные хуки.
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
- Не содержит бизнес-логику или сценарную логику.
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент).
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
```text
user/
├── ui/
│ ├── user-avatar/
│ │ ├── user-avatar.tsx
│ │ ├── styles/
│ │ │ └── user-avatar.module.css
│ │ ├── types/
│ │ │ └── user-avatar-props.type.ts
│ │ └── index.ts
│ └── user-status/
│ ├── user-status.tsx
│ └── index.ts
├── types/
├── hooks/
├── user.tsx
└── index.ts
```
Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `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/` — компонент родительского модуля без собственной архитектурной ответственности.
Вложенность `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
```

View File

@@ -0,0 +1,249 @@
---
title: Композиция через Provider
description: "Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя."
---
# Композиция через Provider
Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя.
## Идея
Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние `parts/` достают нужные домены через хук без пропс-дриллинга.
## Принципы
1. **Принадлежность.** Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах.
2. **Внутренний тип.** Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей.
3. **Внутренний хук.** Хук доступа не экспортируется — доступен только внутри screen и его `parts/`.
4. **Публичный Provider.** Только Provider экспортируется через `index.ts`, чтобы роутер мог обернуть screen.
5. **Сборка снаружи.** Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает.
6. **Запрет для бизнеса.** Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики.
## Структура модуля
```text
screens/main/
├── main.screen.tsx
├── providers/
│ └── main-composition.provider.tsx
├── hooks/
│ └── use-main-composition.hook.ts
├── types/
│ └── main-composition.type.ts
├── parts/
│ └── featured-products/
│ ├── featured-products.tsx
│ └── index.ts
└── index.ts
```
Сегмент `providers/` — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются.
## Распределение по сегментам
| Файл | Сегмент | Назначение |
|------|---------|------------|
| `main-composition.type.ts` | `types/` | TypeScript-тип композиции |
| `main-composition.provider.tsx` | `providers/` | Context и Provider-компонент |
| `use-main-composition.hook.ts` | `hooks/` | React-хук доступа |
| `main.screen.tsx` | корень | Корневой компонент screen-модуля |
| `featured-products/` | `parts/` | Вложенный модуль со своим публичным API |
## Тип композиции
Файл: `screens/main/types/main-composition.type.ts`.
Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через `index.ts`, чтобы другие модули не зависели от внутренней формы композиции screen.
```ts
import type { CatalogApi } from '@/business/catalog'
import type { CartApi } from '@/business/cart'
export type MainComposition = {
catalog: CatalogApi
cart: CartApi
}
```
## Context и Provider
Файл: `screens/main/providers/main-composition.provider.tsx`.
Context — внутренняя деталь Provider, наружу он не экспортируется. Значение `null` по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве.
Provider-компонент экспортируется через `index.ts`. Роутер передаёт в `value` уже собранный граф фабрик со стабильной ссылкой.
```tsx
import { createContext, type ReactNode } from 'react'
import type { MainComposition } from '../types/main-composition.type'
export const MainCompositionContext = createContext<MainComposition | null>(null)
type Props = {
value: MainComposition
children: ReactNode
}
export const MainCompositionProvider = ({ value, children }: Props) => (
<MainCompositionContext.Provider value={value}>
{children}
</MainCompositionContext.Provider>
)
```
## Хук доступа
Файл: `screens/main/hooks/use-main-composition.hook.ts`.
Хук остаётся внутренним и не экспортируется через `index.ts` модуля. Он доступен только внутри screen и его `parts/`.
Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева.
```ts
import { useContext } from 'react'
import { MainCompositionContext } from '../providers/main-composition.provider'
export const useMainComposition = () => {
const ctx = useContext(MainCompositionContext)
if (!ctx) {
throw new Error('useMainComposition must be used within MainCompositionProvider')
}
return ctx
}
```
## Сборка графа в роутере
Файл: `app/router.tsx`.
Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики.
Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента.
```tsx
import { MainScreen, MainCompositionProvider } from '@/screens/main'
import { catalogFactory } from '@/business/catalog'
import { cartFactory } from '@/business/cart'
import { authFactory } from '@/business/auth'
const auth = authFactory()
const catalog = catalogFactory()
const cart = cartFactory({ auth })
const MainRoute = () => (
<MainCompositionProvider value={{ catalog, cart }}>
<MainScreen />
</MainCompositionProvider>
)
```
## Корневой компонент screen
Файл: `screens/main/main.screen.tsx`.
Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные `useCategories` и `CategoryList`, а не обращение к фабричному API через точку.
```tsx
import { useMainComposition } from './hooks/use-main-composition.hook'
import { FeaturedProducts } from './parts/featured-products'
export const MainScreen = () => {
const { catalog } = useMainComposition()
const { useCategories, CategoryList } = catalog
const categories = useCategories()
return (
<div>
<CategoryList categories={categories} />
<FeaturedProducts />
</div>
)
}
```
## Вложенный part
Файл: `screens/main/parts/featured-products/featured-products.tsx`.
Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props.
Из API доменов достаются готовые сущности: `useFeatured`, `ProductCard` и `addItem`. Компонент работает с ними напрямую.
```tsx
import { useMainComposition } from '../../hooks/use-main-composition.hook'
export const FeaturedProducts = () => {
const { catalog, cart } = useMainComposition()
const { useFeatured, ProductCard } = catalog
const { addItem } = cart
const products = useFeatured()
return (
<div>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
onAdd={() => addItem(product.id)}
/>
))}
</div>
)
}
```
Файл: `screens/main/parts/featured-products/index.ts`.
```ts
export { FeaturedProducts } from './featured-products'
```
## Публичный API screen-модуля
Файл: `screens/main/index.ts`.
Наружу экспортируются только screen и его Provider. `MainComposition`, `MainCompositionContext` и `useMainComposition` остаются деталями реализации.
```ts
export { MainScreen } from './main.screen'
export { MainCompositionProvider } from './providers/main-composition.provider'
```
## Почему тип композиции не экспортируется
Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen.
Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen.
```ts
import type { MainComposition } from '@/screens/main'
```
Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля.
## Почему хук не экспортируется
Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его `parts/`.
## Почему Provider экспортируется
Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева.
## Стабильность value
Фабрики создаются на уровне модуля, поэтому `catalog` и `cart` сохраняют ссылки между рендерами `MainRoute`.
Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию.
## Расширение на другие screen-модули
Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов.
```text
screens/checkout/providers/checkout-composition.provider.tsx
screens/checkout/hooks/use-checkout-composition.hook.ts
screens/checkout/types/checkout-composition.type.ts
```
Имена включают имя screen-модуля. Не используйте универсальные названия вроде `useComposition` или `useScope`: по имени файла должно быть понятно, к какой странице привязан Context.

View File

@@ -0,0 +1,52 @@
---
title: Композиция фабрик
description: "Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов."
---
# Композиция фабрик
Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов.
## Идея
Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик.
## Структура screen-модуля
```text
screens/home/
├── home.screen.tsx
└── index.ts
```
## Сборка фабрик
Файл: `screens/home/home.screen.tsx`.
```tsx
import { customerFactory } from '@/business/customer'
import { orderFactory } from '@/business/order'
const customer = customerFactory()
const order = orderFactory({ customer })
const { useOrder, OrderCard } = order
export const HomeScreen = () => {
const currentOrder = useOrder()
return <OrderCard order={currentOrder} />
}
```
`customerFactory` создаётся первой, потому что `orderFactory` зависит от части API домена `customer`. Модуль `order` не импортирует `customer` в runtime — зависимость передаётся снаружи.
## Публичный API screen-модуля
Файл: `screens/home/index.ts`.
```ts
export { HomeScreen } from './home.screen'
```
Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля.

View File

@@ -0,0 +1,114 @@
---
title: Создание фабрики
description: "Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API."
---
# Создание фабрики
Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API.
## Структура business-модуля
Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в `types/`.
```text
business/customer/
├── customer.factory.ts
├── hooks/
├── types/
│ ├── customer.type.ts
│ ├── customer-api.type.ts
│ └── customer-factory.type.ts
├── ui/
└── index.ts
```
## Тип публичного API
Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы.
```ts
// business/customer/types/customer-api.type.ts
import type { ReactNode } from 'react'
import type { Customer } from './customer.type'
export type CustomerCardProps = {
customer: Customer
}
export type CustomerApi = {
useCustomer: () => Customer | null
CustomerCard: (props: CustomerCardProps) => ReactNode
}
```
```ts
// business/customer/types/customer-factory.type.ts
import type { CustomerApi } from './customer-api.type'
export type CustomerFactory = () => CustomerApi
```
## Фабрика без зависимостей
Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов.
```ts
// business/customer/customer.factory.ts
import { useCustomer } from './hooks/use-customer.hook'
import { CustomerCard } from './ui/customer-card'
import type { CustomerFactory } from './types/customer-factory.type'
export const customerFactory: CustomerFactory = () => {
return {
useCustomer,
CustomerCard,
}
}
```
```ts
// business/customer/index.ts
export { customerFactory } from './customer.factory'
export type { Customer } from './types/customer.type'
export type { CustomerApi } from './types/customer-api.type'
export type { CustomerFactory } from './types/customer-factory.type'
```
## Фабрика с зависимостями
Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API.
```ts
// business/order/types/order-deps.type.ts
import type { CustomerApi } from '@/business/customer'
export type OrderDeps = {
customer: Pick<CustomerApi, 'useCustomer'>
}
```
```ts
// business/order/types/order-factory.type.ts
import type { OrderApi } from './order-api.type'
import type { OrderDeps } from './order-deps.type'
export type OrderFactory = (deps: OrderDeps) => OrderApi
```
```ts
// business/order/order.factory.ts
import { createUseOrder } from './hooks/use-order.hook'
import { OrderCard } from './ui/order-card'
import type { OrderFactory } from './types/order-factory.type'
export const orderFactory: OrderFactory = (deps) => {
const useOrder = createUseOrder(deps)
return {
useOrder,
OrderCard,
}
}
```

View File

@@ -0,0 +1,8 @@
---
title: Подсказки
description: Короткие ответы на типовые вопросы и решения для спорных ситуаций.
---
# Подсказки
Короткие ответы на типовые вопросы и решения для спорных ситуаций.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,347 @@
import {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBaseVNode,
createBlock,
createCommentVNode,
createElementBlock,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
} from "./chunk-BKAC4ZFU.js";
export {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBlock,
createCommentVNode,
createElementBlock,
createBaseVNode as createElementVNode,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
};
//# sourceMappingURL=vue.js.map

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vitepress';
import llmstxt from 'vitepress-plugin-llms';
import { sidebar, site } from '../docs.config';
export default defineConfig({
title: site.title,
description: site.description,
base: site.base,
outDir: site.outDir,
srcDir: 'content',
cleanUrls: true,
vite: {
plugins: [llmstxt()],
},
themeConfig: {
sidebar,
socialLinks: [],
},
});

View File

@@ -0,0 +1,66 @@
export const site = {
title: 'SLM Design',
description: 'Каноны архитектуры SLM Design',
base: '/slm-design/',
outDir: '../../public/slm-design',
};
export const mounts = [
{
source: 'slm-design/architecture/index.md',
target: 'index.md',
},
{
source: 'slm-design/architecture/index.md',
target: 'architecture/index.md',
},
{
source: 'slm-design/architecture/layers.md',
target: 'architecture/layers.md',
},
{
source: 'slm-design/architecture/modules.md',
target: 'architecture/modules.md',
},
{
source: 'slm-design/architecture/segments.md',
target: 'architecture/segments.md',
},
{
source: 'slm-design/architecture/monorepo.md',
target: 'architecture/monorepo.md',
},
{
source: 'slm-design/examples/react/factory.md',
target: 'examples/react/factory.md',
},
{
source: 'slm-design/examples/react/factory-composition.md',
target: 'examples/react/factory-composition.md',
},
{
source: 'slm-design/examples/react/composition-provider.md',
target: 'examples/react/composition-provider.md',
},
];
export const sidebar = [
{
text: 'Архитектура',
items: [
{ text: 'Обзор', link: '/architecture/' },
{ text: 'Слои', link: '/architecture/layers' },
{ text: 'Модули', link: '/architecture/modules' },
{ text: 'Сегменты', link: '/architecture/segments' },
{ text: 'Монорепозитории', link: '/architecture/monorepo' },
],
},
{
text: 'Примеры React',
items: [
{ text: 'Создание фабрики', link: '/examples/react/factory' },
{ text: 'Композиция фабрик', link: '/examples/react/factory-composition' },
{ text: 'Композиция через Provider', link: '/examples/react/composition-provider' },
],
},
];

22
eslint.config.js Normal file
View File

@@ -0,0 +1,22 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
},
},
])

14
index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Единое пространство для идей, черновиков и первых версий документаций" />
<title>Документация</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6230
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "all-docs",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "npm run site:generate && tsc -b && vite build",
"docs:prepare:slm-design": "tsx scripts/docs/prepare.ts slm-design",
"docs:build:slm-design": "npm run docs:prepare:slm-design && vitepress build docs/slm-design",
"site:generate": "tsx scripts/site/generate-artifacts.ts",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"tsx": "^4.21.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.59.2",
"vite": "^5.4.21",
"vitepress": "^1.6.3",
"vitepress-plugin-llms": "^1.12.2"
}
}

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
public/icons.svg Normal file
View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

85
scripts/docs/prepare.ts Normal file
View File

@@ -0,0 +1,85 @@
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
type Page = {
source: string;
target: string;
};
type DocsConfig = {
mounts: Page[];
};
const MD_LINK_RE = /\]\((?!#|[a-z][a-z0-9+.-]*:)([^)\s]+\.md)(#[^)]*)?\)/gi;
const siteName = process.argv[2];
if (!siteName) {
throw new Error('Укажите имя сайта: tsx scripts/docs/prepare.ts slm-design');
}
const rootDir = process.cwd();
const canonsDir = path.join(rootDir, 'canons');
const siteDir = path.join(rootDir, 'docs', siteName);
const contentDir = path.join(siteDir, 'content');
const configPath = path.join(siteDir, 'docs.config.ts');
if (!fs.existsSync(configPath)) {
throw new Error(`Не найден конфиг сайта: ${configPath}`);
}
const config = (await import(pathToFileURL(configPath).href)) as DocsConfig;
const targetBySource = new Map(
config.mounts.map((page) => [normalizePath(page.source), normalizePath(page.target)]),
);
function normalizePath(value: string) {
return value.split(path.sep).join('/').replace(/^\.\//, '');
}
function formatRelativeMarkdownPath(fromTarget: string, toTarget: string) {
const relative = path
.relative(path.dirname(fromTarget), toTarget)
.split(path.sep)
.join('/');
return relative.startsWith('.') ? relative : `./${relative}`;
}
function transformMarkdownLinks(content: string, page: Page) {
const sourceDir = path.posix.dirname(normalizePath(page.source));
return content.replace(MD_LINK_RE, (match, href: string, hash = '') => {
const [hrefPath, query = ''] = href.split('?');
const sourcePath = normalizePath(path.posix.normalize(path.posix.join(sourceDir, hrefPath)));
const target = targetBySource.get(sourcePath);
if (!target) return match;
const nextHref = formatRelativeMarkdownPath(page.target, `${target}${query ? `?${query}` : ''}`);
return `](${nextHref}${hash})`;
});
}
fs.rmSync(contentDir, { recursive: true, force: true });
fs.mkdirSync(contentDir, { recursive: true });
for (const page of config.mounts) {
const sourcePath = path.join(canonsDir, page.source);
const targetPath = path.join(contentDir, page.target);
if (!fs.existsSync(sourcePath)) {
throw new Error(`Не найден канон: ${sourcePath}`);
}
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
const content = transformMarkdownLinks(fs.readFileSync(sourcePath, 'utf8'), page);
fs.writeFileSync(targetPath, content, 'utf8');
console.log(`${page.target} -> canons/${page.source}`);
}
console.log(`Подготовлен VitePress content для ${siteName}: ${config.mounts.length} страниц`);

View File

@@ -0,0 +1,65 @@
import fs from 'node:fs';
import path from 'node:path';
import { docs } from '../../src/config/docs.config';
const siteTitle = 'Документация';
const siteDescription = 'Единое пространство для идей, черновиков и первых версий документаций, которые ещё формируются и постепенно становятся самостоятельными материалами.';
const rootDir = process.cwd();
const publicDir = path.join(rootDir, 'public');
const llmsPath = path.join(publicDir, 'llms.txt');
function formatMarkdownLink(label: string, href: string, description: string) {
return `- [${label}](${href}): ${description}`;
}
function findDocLink(doc: (typeof docs)[number], label: string) {
return doc.links.find((link) => link.label === label);
}
function formatLlmsLinks() {
return docs
.map((doc) => {
const link = findDocLink(doc, 'llms.txt');
if (!link) return undefined;
return formatMarkdownLink(doc.title, link.href, doc.description);
})
.filter(Boolean);
}
function formatFullLinks() {
return docs
.map((doc) => {
const link = findDocLink(doc, 'llms-full.txt');
if (!link) return undefined;
return formatMarkdownLink(`${doc.title} full`, link.href, `Полный bundle документации: ${doc.label.toLowerCase()}.`);
})
.filter(Boolean);
}
const content = [
`# ${siteTitle}`,
'',
`> ${siteDescription}`,
'',
'Этот файл является корневой картой документаций. Для работы с конкретным направлением используйте его собственный `llms.txt`.',
'',
'## Documentation',
'',
...formatLlmsLinks(),
'',
'## Optional',
'',
...formatFullLinks(),
'',
].join('\n');
fs.mkdirSync(publicDir, { recursive: true });
fs.writeFileSync(llmsPath, content, 'utf8');
console.log(`Сгенерирован ${path.relative(rootDir, llmsPath)}`);

465
src/App.css Normal file
View File

@@ -0,0 +1,465 @@
.page {
display: flex;
flex-direction: column;
justify-content: center;
gap: 64px;
min-height: 100vh;
padding: 48px 32px;
background:
radial-gradient(circle at 50% 18%, var(--hero-glow), transparent 34%),
linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px),
var(--page-bg);
background-size: auto, 32px 32px, 32px 32px, auto;
background-position: center, center, center, center;
color: var(--text-primary);
}
.hero {
text-align: center;
}
.title {
margin: 0 0 16px;
color: var(--accent-cool);
font-size: 56px;
font-weight: 750;
letter-spacing: -0.035em;
line-height: 1;
}
.lead {
max-width: 760px;
margin: 0 auto;
color: var(--text-secondary);
font-size: 18px;
line-height: 1.55;
}
.controls {
display: inline-flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 28px;
}
.repoLink {
display: inline-flex;
align-items: center;
gap: 8px;
height: 36px;
padding: 0 14px;
border: 1px solid var(--border-soft);
border-radius: 999px;
color: var(--text-secondary);
background: var(--surface-soft);
box-shadow: 0 1px 2px var(--shadow-subtle);
font-size: 13px;
font-weight: 700;
text-decoration: none;
transition: border-color 150ms ease, color 150ms ease, background 150ms ease;
}
.repoLink:hover {
border-color: color-mix(in srgb, var(--accent-cool) 46%, var(--border-soft));
color: var(--text-primary);
background: color-mix(in srgb, var(--surface-soft) 92%, var(--accent-cool));
}
.repoLink:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 3px;
}
.themeToggle {
display: inline-flex;
align-items: stretch;
gap: 2px;
height: 36px;
padding: 4px;
border: 1px solid var(--border-soft);
border-radius: 999px;
background: var(--surface-soft);
box-shadow: 0 1px 2px var(--shadow-subtle);
}
.themeToggleOption {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
padding: 0;
border: 0;
border-radius: 999px;
color: var(--text-muted);
background: transparent;
font: inherit;
cursor: pointer;
transition: background-color 150ms ease, color 150ms ease, box-shadow 150ms ease;
}
.themeToggleOption:hover {
color: var(--text-primary);
}
.themeToggleOption:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 3px;
}
.themeToggleOption[data-active='true'] {
color: var(--text-primary);
background: var(--page-bg);
box-shadow: 0 1px 2px var(--shadow-subtle);
}
.themeToggleIcon {
display: inline-flex;
width: 16px;
height: 16px;
}
.themeToggleIcon svg {
width: 100%;
height: 100%;
}
.docsPanel {
display: grid;
gap: 14px;
width: 100%;
max-width: 920px;
margin: 0 auto;
}
.docsPanelHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 0 4px 2px;
color: var(--text-muted);
font-size: 12px;
font-weight: 650;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.docItem {
--doc-accent: var(--accent-cool);
position: relative;
display: grid;
gap: 18px;
padding: 24px;
overflow: hidden;
border: 1px solid var(--border-soft);
border-radius: 20px;
color: inherit;
background: color-mix(in srgb, var(--surface-soft) 98%, var(--doc-accent));
box-shadow: 0 18px 54px rgb(0 0 0 / 9%);
transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
}
.docItem[data-accent='blue'] {
--doc-accent: #62a5ff;
}
.docItem[data-accent='cyan'] {
--doc-accent: #57d6ff;
}
.docItem[data-accent='pink'] {
--doc-accent: #ff72c6;
}
.docItem:hover {
border-color: color-mix(in srgb, var(--doc-accent) 58%, var(--border-soft));
background: color-mix(in srgb, var(--surface-soft) 94%, var(--doc-accent));
box-shadow:
0 18px 54px rgb(0 0 0 / 10%),
0 0 0 1px color-mix(in srgb, var(--doc-accent) 18%, transparent),
0 0 36px color-mix(in srgb, var(--doc-accent) 14%, transparent);
}
.docCardLink {
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
}
.docCardLink:focus-visible {
outline: 2px solid var(--doc-accent);
outline-offset: -3px;
}
.docMain {
position: relative;
z-index: 0;
display: grid;
grid-template-columns: 64px minmax(0, 1fr);
gap: 18px;
align-items: center;
}
.docMark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
color: var(--doc-accent);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 13px;
font-weight: 760;
letter-spacing: -0.03em;
}
.docIcon {
width: 64px;
height: 64px;
}
.docIconSlm {
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.35;
}
.docIconSlm circle {
fill: currentColor;
stroke: none;
}
.docIconNext path:first-child {
fill: currentColor;
}
.docIconNext path:last-child {
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-width: 1.8;
}
.docIconReact {
fill: none;
stroke: currentColor;
stroke-width: 1.25;
}
.docIconReact circle {
fill: currentColor;
stroke: none;
}
.docIconFigma {
fill: none;
stroke: currentColor;
stroke-width: 1.3;
}
.docIconFigma circle {
fill: color-mix(in srgb, currentColor 18%, transparent);
}
.docMeta {
margin-bottom: 5px;
color: var(--doc-accent);
font-size: 11px;
font-weight: 760;
letter-spacing: 0.09em;
line-height: 1;
text-transform: uppercase;
}
.docItem h2 {
margin: 0;
color: var(--text-primary);
font-size: 21px;
font-weight: 760;
letter-spacing: -0.03em;
line-height: 1.15;
}
.docItem p {
max-width: 560px;
margin: 8px 0 0;
color: var(--text-secondary);
font-size: 14px;
line-height: 1.55;
}
.docActions {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
padding-top: 16px;
border-top: 1px solid var(--border-soft);
pointer-events: none;
}
.docStatus,
.docLink {
display: inline-flex;
align-items: center;
text-decoration: none;
pointer-events: auto;
}
.docStatus {
gap: 8px;
padding: 6px 10px;
border: 1px solid color-mix(in srgb, var(--doc-accent) 34%, var(--border-soft));
border-radius: 10px;
color: var(--doc-accent);
background: color-mix(in srgb, var(--surface-soft) 90%, var(--doc-accent));
font-size: 12px;
font-weight: 700;
transition: border-color 150ms ease, background 150ms ease;
}
.docStatusLink {
padding: 7px 12px;
border-color: color-mix(in srgb, var(--accent-cool) 58%, var(--border-soft));
border-radius: 10px;
color: var(--accent-cool);
background: color-mix(in srgb, var(--surface-soft) 88%, var(--accent-cool));
font-size: 13px;
}
.docStatusLink:hover {
border-color: var(--accent-cool);
background: color-mix(in srgb, var(--surface-soft) 80%, var(--accent-cool));
}
.docItem[data-state='planned'] .docStatus {
color: var(--text-muted);
}
.docLinks {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.docLink {
padding: 6px 10px;
border: 1px solid var(--border-soft);
border-radius: 8px;
color: var(--text-primary);
background: var(--page-bg);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
font-weight: 500;
transition: border-color 150ms ease, color 150ms ease;
}
.docLink:hover {
border-color: var(--doc-accent);
color: var(--doc-accent);
}
.footer {
margin: 0;
color: var(--text-muted);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
text-align: center;
}
@media (max-width: 768px) {
.page {
justify-content: flex-start;
gap: 40px;
padding: 48px 20px 56px;
}
.title {
font-size: 38px;
}
.lead {
font-size: 16px;
}
.docItem {
gap: 16px;
}
.docActions {
align-items: flex-start;
flex-direction: column;
}
}
@media (max-width: 480px) {
.page {
gap: 36px;
padding: 44px 16px 48px;
}
.title {
font-size: 32px;
}
.lead {
font-size: 15px;
line-height: 1.5;
}
.docItem {
padding-right: 16px;
padding-left: 16px;
}
.docsPanelHeader {
padding-right: 4px;
padding-left: 4px;
}
.docMain {
grid-template-columns: 42px minmax(0, 1fr);
gap: 12px;
}
.docMark {
width: 42px;
height: 42px;
font-size: 12px;
}
.docIcon {
width: 30px;
height: 30px;
}
.docItem h2 {
font-size: 19px;
}
.docStatus,
.docLinks,
.docLink {
width: 100%;
}
.docStatus,
.docLink {
justify-content: center;
}
.docLink {
flex: 1;
}
}

290
src/App.tsx Normal file
View File

@@ -0,0 +1,290 @@
import { useEffect, useLayoutEffect, useState } from 'react'
import { docs } from './config/docs.config'
import './App.css'
type ThemeMode = 'system' | 'dark' | 'light'
type ResolvedTheme = Exclude<ThemeMode, 'system'>
const DARK_THEME_QUERY = '(prefers-color-scheme: dark)'
const THEME_STORAGE_KEY = 'all-docs-theme'
const repositoryUrl = 'https://gromlab.ru/gromov/docs'
const themeOptions: ReadonlyArray<{
value: Exclude<ThemeMode, 'system'>
ariaLabel: string
title: string
}> = [
{
value: 'light',
ariaLabel: 'Включить светлую тему',
title: 'Светлая',
},
{
value: 'dark',
ariaLabel: 'Включить тёмную тему',
title: 'Тёмная',
},
]
const themeIconByMode = {
light: (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="2" />
<path
d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"
stroke="currentColor"
strokeLinecap="round"
strokeWidth="2"
/>
</svg>
),
dark: (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none">
<path
d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
),
}
const canUseDom = () => typeof window !== 'undefined' && typeof document !== 'undefined'
const isThemeMode = (value: string | null): value is ThemeMode => {
return value === 'system' || value === 'dark' || value === 'light'
}
const getSystemTheme = (): ResolvedTheme => {
if (!canUseDom()) return 'light'
return window.matchMedia(DARK_THEME_QUERY).matches ? 'dark' : 'light'
}
const resolveTheme = (theme: ThemeMode): ResolvedTheme => {
return theme === 'system' ? getSystemTheme() : theme
}
const getStoredTheme = (): ThemeMode => {
if (!canUseDom()) return 'dark'
const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY)
return isThemeMode(storedTheme) ? storedTheme : 'dark'
}
const applyTheme = (theme: ThemeMode) => {
if (!canUseDom()) return
const resolvedTheme = resolveTheme(theme)
document.documentElement.dataset.theme = theme
document.documentElement.style.colorScheme = resolvedTheme
if (theme === 'system') {
window.localStorage.removeItem(THEME_STORAGE_KEY)
return
}
window.localStorage.setItem(THEME_STORAGE_KEY, theme)
}
function useTheme() {
const [theme, setTheme] = useState<ThemeMode>(getStoredTheme)
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => resolveTheme(getStoredTheme()))
useLayoutEffect(() => {
applyTheme(theme)
setResolvedTheme(resolveTheme(theme))
}, [theme])
useEffect(() => {
if (!canUseDom()) return undefined
const mediaQuery = window.matchMedia(DARK_THEME_QUERY)
const handleSystemThemeChange = () => {
if (theme !== 'system') return
const nextResolvedTheme = resolveTheme(theme)
document.documentElement.style.colorScheme = nextResolvedTheme
setResolvedTheme(nextResolvedTheme)
}
mediaQuery.addEventListener('change', handleSystemThemeChange)
return () => mediaQuery.removeEventListener('change', handleSystemThemeChange)
}, [theme])
return { theme, resolvedTheme, setTheme }
}
function ThemeToggle() {
const { theme, resolvedTheme, setTheme } = useTheme()
const toggleTheme = (value: Exclude<ThemeMode, 'system'>) => {
setTheme(theme === value ? 'system' : value)
}
return (
<div className="themeToggle" role="group" aria-label="Тема" data-resolved-theme={resolvedTheme}>
{themeOptions.map((option) => (
<button
className="themeToggleOption"
type="button"
aria-pressed={theme === option.value}
aria-label={option.ariaLabel}
title={option.title}
data-active={theme === option.value}
key={option.value}
onClick={() => toggleTheme(option.value)}
>
<span className="themeToggleIcon">{themeIconByMode[option.value]}</span>
</button>
))}
</div>
)
}
function GithubIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .3a12 12 0 0 0-3.8 23.38c.6.12.83-.26.83-.57L9 21.07c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.08-.74.09-.73.09-.73 1.2.09 1.83 1.24 1.83 1.24 1.08 1.83 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.14-.3-.54-1.52.1-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.28-1.55 3.29-1.23 3.29-1.23.64 1.66.24 2.88.12 3.18a4.65 4.65 0 0 1 1.23 3.22c0 4.61-2.8 5.63-5.48 5.92.42.36.81 1.1.81 2.22l-.01 3.29c0 .31.2.69.82.57A12 12 0 0 0 12 .3Z" />
</svg>
)
}
function DocIcon({ mark }: { mark: string }) {
if (mark === 'SLM') {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" className="docIcon docIconSlm">
<path d="M5 8.2 12 4.5l7 3.7-7 3.7-7-3.7Z" />
<path d="M5 12 12 15.7 19 12" />
<path d="M5 15.8 12 19.5l7-3.7" />
<circle cx="12" cy="8.2" r="1" />
<circle cx="8.8" cy="13.8" r=".75" />
<circle cx="15.2" cy="13.8" r=".75" />
</svg>
)
}
if (mark === 'NX') {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" className="docIcon docIconNext">
<path d="M7 6.5h2.35l5.42 8.28V6.5H17v11h-2.35L9.23 9.22v8.28H7v-11Z" />
<path d="M15.1 15.22 19 20" />
</svg>
)
}
if (mark === 'RE') {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" className="docIcon docIconReact">
<circle cx="12" cy="12" r="1.8" />
<ellipse cx="12" cy="12" rx="8.6" ry="3.3" />
<ellipse cx="12" cy="12" rx="8.6" ry="3.3" transform="rotate(60 12 12)" />
<ellipse cx="12" cy="12" rx="8.6" ry="3.3" transform="rotate(120 12 12)" />
</svg>
)
}
if (mark === 'FG') {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" className="docIcon docIconFigma">
<circle cx="9" cy="5" r="3" />
<circle cx="15" cy="5" r="3" />
<circle cx="9" cy="12" r="3" />
<circle cx="15" cy="12" r="3" />
<circle cx="9" cy="19" r="3" />
</svg>
)
}
return <span>{mark}</span>
}
function App() {
return (
<main className="page">
<section className="hero" aria-labelledby="page-title">
<h1 className="title" id="page-title">Документация</h1>
<p className="lead">
Единое пространство для идей, черновиков и первых версий документаций,
которые ещё формируются и постепенно становятся самостоятельными материалами.
</p>
<div className="controls">
<a className="repoLink" href={repositoryUrl} target="_blank" rel="noopener noreferrer">
<GithubIcon />
<span>Репозиторий</span>
</a>
<ThemeToggle />
</div>
</section>
<section className="docsPanel" aria-label="Быстрые переходы">
<div className="docsPanelHeader">
<span>Документация</span>
<span>{docs.length} направления</span>
</div>
{docs.map((doc) => {
const isAvailable = Boolean(doc.href)
return (
<article
className="docItem"
data-accent={doc.accent}
data-state={isAvailable ? 'available' : 'planned'}
key={doc.href ?? doc.title}
>
{isAvailable && (
<a className="docCardLink" href={doc.href} aria-label={`Открыть ${doc.title}`} />
)}
<div className="docMain">
<span className="docMark" aria-hidden="true">
<DocIcon mark={doc.mark} />
</span>
<div>
<div className="docMeta">
{doc.label}
</div>
<h2>{doc.title}</h2>
<p>{doc.description}</p>
</div>
</div>
<div className="docActions">
{isAvailable ? (
<a className="docStatus docStatusLink" href={doc.href}>
Открыть -&gt;
</a>
) : (
<span className="docStatus" aria-disabled="true">
{doc.status}
</span>
)}
{doc.links.length > 0 && (
<div className="docLinks" aria-label="LLM-артефакты">
{doc.links.map((link) => (
<a className="docLink" href={link.href} key={link.href}>
{link.label}
</a>
))}
</div>
)}
</div>
</article>
)
})}
</section>
<footer className="footer">Рабочее пространство документаций</footer>
</main>
)
}
export default App

BIN
src/assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

1
src/assets/react.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

1
src/assets/vite.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

67
src/config/docs.config.ts Normal file
View File

@@ -0,0 +1,67 @@
export type DocLink = {
label: string
href: string
}
export type DocCard = {
title: string
label: string
mark: string
description: string
href?: string
status: string
accent: string
links: DocLink[]
}
export const docs: DocCard[] = [
{
title: 'SLM Design',
label: 'Архитектура',
mark: 'SLM',
description: 'Архитектура frontend-приложений, где слои задают направление зависимостей, модули становятся границами ответственности, а явный DI через фабрики удерживает домены изолированными и предсказуемыми.',
href: '/slm-design/',
status: 'Доступно',
accent: 'violet',
links: [
{ label: 'llms.txt', href: '/slm-design/llms.txt' },
{ label: 'llms-full.txt', href: '/slm-design/llms-full.txt' },
],
},
{
title: 'NextJS Style Guide',
label: 'Стиль проекта',
mark: 'NX',
description: 'Правила организации Next.js-приложений, роутинга, серверных границ и проектных соглашений.',
status: 'Скоро',
accent: 'blue',
links: [
{ label: 'llms.txt', href: '/nextjs-style-guide/llms.txt' },
{ label: 'llms-full.txt', href: '/nextjs-style-guide/llms-full.txt' },
],
},
{
title: 'React Style Guide',
label: 'Стиль кода',
mark: 'RE',
description: 'Практики написания React-компонентов, хуков, состояния и клиентского UI-кода.',
status: 'Скоро',
accent: 'cyan',
links: [
{ label: 'llms.txt', href: '/react-style-guide/llms.txt' },
{ label: 'llms-full.txt', href: '/react-style-guide/llms-full.txt' },
],
},
{
title: 'Figma Adaptive Standards',
label: 'Макеты',
mark: 'FG',
description: 'Стандарты и требования к подготовке адаптивных макетов в Figma: брейкпоинты, ресайз в диапазоне, Auto Layout/Constraints, компоненты, сетка, типографика, состояния UI, A11y и передача в разработку.',
status: 'Скоро',
accent: 'pink',
links: [
{ label: 'llms.txt', href: '/figma-adaptive-standards/llms.txt' },
{ label: 'llms-full.txt', href: '/figma-adaptive-standards/llms-full.txt' },
],
},
]

83
src/index.css Normal file
View File

@@ -0,0 +1,83 @@
:root {
color-scheme: light;
--sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--page-bg: #eef1f7;
--surface-soft: #f9fbff;
--text-primary: #101522;
--text-secondary: #424b60;
--text-muted: #7d879b;
--border-soft: #cfd6e5;
--accent-cool: #5866dc;
--grid-line: rgb(196 204 222 / 0.48);
--hero-glow: rgb(88 102 220 / 0.12);
--focus-ring: rgba(102, 119, 255, 0.56);
--shadow-subtle: rgba(25, 31, 54, 0.1);
font-family: var(--sans);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
:root[data-theme='dark'] {
color-scheme: dark;
--page-bg: #1b1c21;
--surface-soft: #202229;
--text-primary: #f1f3f8;
--text-secondary: #a9afbf;
--text-muted: #747c90;
--border-soft: #343741;
--accent-cool: #8492ff;
--grid-line: rgb(72 76 92 / 0.34);
--hero-glow: rgb(132 146 255 / 0.18);
--focus-ring: rgba(132, 146, 255, 0.66);
--shadow-subtle: rgba(0, 0, 0, 0.24);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
color-scheme: dark;
--page-bg: #1b1c21;
--surface-soft: #202229;
--text-primary: #f1f3f8;
--text-secondary: #a9afbf;
--text-muted: #747c90;
--border-soft: #343741;
--accent-cool: #8492ff;
--grid-line: rgb(72 76 92 / 0.34);
--hero-glow: rgb(132 146 255 / 0.18);
--focus-ring: rgba(132, 146, 255, 0.66);
--shadow-subtle: rgba(0, 0, 0, 0.24);
}
}
* {
box-sizing: border-box;
}
html {
min-width: 320px;
background: var(--page-bg);
}
body {
min-width: 320px;
min-height: 100svh;
margin: 0;
background: var(--page-bg);
color: var(--text-primary);
}
button,
a {
font: inherit;
}
p {
margin: 0;
}
#root {
min-height: 100svh;
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

Some files were not shown because too many files have changed in this diff Show More