feat: добавить хаб документаций
- добавлен React/Vite-лендинг с карточками документаций - добавлена генерация корневого llms.txt из конфига документов - добавлена сборка SLM Design через VitePress - добавлены Dockerfile, Caddyfile и Gitea CI/CD - настроены контекстные Link headers для llms.txt
This commit is contained in:
12
.dockerignore
Normal file
12
.dockerignore
Normal 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
126
.gitea/workflows/ci.yml
Normal 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
29
.gitignore
vendored
Normal 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
49
Caddyfile
Normal 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
14
Dockerfile
Normal 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
73
README.md
Normal 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...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
114
canons/slm-design/architecture/index.md
Normal file
114
canons/slm-design/architecture/index.md
Normal 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.
|
||||||
|
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.
|
||||||
254
canons/slm-design/architecture/layers.md
Normal file
254
canons/slm-design/architecture/layers.md
Normal 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-состояния
|
||||||
215
canons/slm-design/architecture/modules.md
Normal file
215
canons/slm-design/architecture/modules.md
Normal 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/`
|
||||||
|
|
||||||
|
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||||
235
canons/slm-design/architecture/monorepo.md
Normal file
235
canons/slm-design/architecture/monorepo.md
Normal 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/*`.
|
||||||
181
canons/slm-design/architecture/segments.md
Normal file
181
canons/slm-design/architecture/segments.md
Normal 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
|
||||||
|
```
|
||||||
249
canons/slm-design/examples/react/composition-provider.md
Normal file
249
canons/slm-design/examples/react/composition-provider.md
Normal 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.
|
||||||
52
canons/slm-design/examples/react/factory-composition.md
Normal file
52
canons/slm-design/examples/react/factory-composition.md
Normal 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-модуля.
|
||||||
114
canons/slm-design/examples/react/factory.md
Normal file
114
canons/slm-design/examples/react/factory.md
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
103
canons/style-guide/DEVELOP.md
Normal file
103
canons/style-guide/DEVELOP.md
Normal 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
59
canons/style-guide/MAP.md
Normal 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-модуль.
|
||||||
77
canons/style-guide/applied/aliases.md
Normal file
77
canons/style-guide/applied/aliases.md
Normal 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'
|
||||||
|
```
|
||||||
114
canons/style-guide/applied/biome.md
Normal file
114
canons/style-guide/applied/biome.md
Normal 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).
|
||||||
165
canons/style-guide/applied/component.md
Normal file
165
canons/style-guide/applied/component.md
Normal 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'
|
||||||
|
```
|
||||||
51
canons/style-guide/applied/creating-project/from-template.md
Normal file
51
canons/style-guide/applied/creating-project/from-template.md
Normal 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/` — решение о репозитории принимает разработчик.
|
||||||
90
canons/style-guide/applied/creating-project/manual.md
Normal file
90
canons/style-guide/applied/creating-project/manual.md
Normal 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 и часть инструментов; полный набор — это эталон, а не обязательство.
|
||||||
112
canons/style-guide/applied/creating-project/nextjs.md
Normal file
112
canons/style-guide/applied/creating-project/nextjs.md
Normal 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`).
|
||||||
121
canons/style-guide/applied/data-fetch/business-composition.md
Normal file
121
canons/style-guide/applied/data-fetch/business-composition.md
Normal 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-модуль отвечает за смысл этих данных в продукте.
|
||||||
88
canons/style-guide/applied/data-fetch/client-get-hook.md
Normal file
88
canons/style-guide/applied/data-fetch/client-get-hook.md
Normal 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).
|
||||||
@@ -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).
|
||||||
100
canons/style-guide/applied/data-fetch/index.md
Normal file
100
canons/style-guide/applied/data-fetch/index.md
Normal 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-модуля.
|
||||||
@@ -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).
|
||||||
64
canons/style-guide/applied/data-fetch/pass-promise-down.md
Normal file
64
canons/style-guide/applied/data-fetch/pass-promise-down.md
Normal 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-клиента.
|
||||||
88
canons/style-guide/applied/data-fetch/server-await.md
Normal file
88
canons/style-guide/applied/data-fetch/server-await.md
Normal 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).
|
||||||
128
canons/style-guide/applied/fonts.md
Normal file
128
canons/style-guide/applied/fonts.md
Normal 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 только импортирует готовый шрифт.
|
||||||
95
canons/style-guide/applied/images.md
Normal file
95
canons/style-guide/applied/images.md
Normal 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
81
canons/style-guide/applied/localization.md
Normal file
81
canons/style-guide/applied/localization.md
Normal 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-модуля.
|
||||||
156
canons/style-guide/applied/module.md
Normal file
156
canons/style-guide/applied/module.md
Normal 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
|
||||||
|
```
|
||||||
186
canons/style-guide/applied/page-level.md
Normal file
186
canons/style-guide/applied/page-level.md
Normal 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
|
||||||
|
```
|
||||||
70
canons/style-guide/applied/postcss.md
Normal file
70
canons/style-guide/applied/postcss.md
Normal 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), раздел «Импорт стилей»).
|
||||||
101
canons/style-guide/applied/project-structure.md
Normal file
101
canons/style-guide/applied/project-structure.md
Normal 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_` доступны в клиентском коде. Остальные доступны только на сервере.
|
||||||
50
canons/style-guide/applied/rest-client/index.md
Normal file
50
canons/style-guide/applied/rest-client/index.md
Normal 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/`.
|
||||||
272
canons/style-guide/applied/rest-client/setup/auto.md
Normal file
272
canons/style-guide/applied/rest-client/setup/auto.md
Normal 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.
|
||||||
313
canons/style-guide/applied/rest-client/setup/hooks.md
Normal file
313
canons/style-guide/applied/rest-client/setup/hooks.md
Normal 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).
|
||||||
88
canons/style-guide/applied/rest-client/setup/index.md
Normal file
88
canons/style-guide/applied/rest-client/setup/index.md
Normal 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/).
|
||||||
198
canons/style-guide/applied/rest-client/setup/manual.md
Normal file
198
canons/style-guide/applied/rest-client/setup/manual.md
Normal 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/).
|
||||||
21
canons/style-guide/applied/rest-client/usage.md
Normal file
21
canons/style-guide/applied/rest-client/usage.md
Normal 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)
|
||||||
|
}
|
||||||
|
```
|
||||||
0
canons/style-guide/applied/stores.md
Normal file
0
canons/style-guide/applied/stores.md
Normal file
176
canons/style-guide/applied/styles/styles-setup.md
Normal file
176
canons/style-guide/applied/styles/styles-setup.md
Normal 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) — стили иконок отдельно от глобальных.
|
||||||
271
canons/style-guide/applied/styles/styles-usage.md
Normal file
271
canons/style-guide/applied/styles/styles-usage.md
Normal 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.
|
||||||
|
- Исключение — нетривиальные хаки и обходные решения, к которым стоит оставить пояснение.
|
||||||
31
canons/style-guide/applied/svg-sprites/svg-sprites-intro.md
Normal file
31
canons/style-guide/applied/svg-sprites/svg-sprites-intro.md
Normal 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/>`, управление цветом.
|
||||||
132
canons/style-guide/applied/svg-sprites/svg-sprites-setup.md
Normal file
132
canons/style-guide/applied/svg-sprites/svg-sprites-setup.md
Normal 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/>`, управление цветом.
|
||||||
56
canons/style-guide/applied/svg-sprites/svg-sprites-usage.md
Normal file
56
canons/style-guide/applied/svg-sprites/svg-sprites-usage.md
Normal 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);
|
||||||
|
}
|
||||||
|
```
|
||||||
97
canons/style-guide/applied/templates/templates-create.md
Normal file
97
canons/style-guide/applied/templates/templates-create.md
Normal 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.
|
||||||
|
|
||||||
|
:::
|
||||||
32
canons/style-guide/applied/templates/templates-intro.md
Normal file
32
canons/style-guide/applied/templates/templates-intro.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: Шаблоны генерации
|
||||||
|
description: "Что такое шаблоны кодогенерации и какие проблемы они решают."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Шаблоны генерации
|
||||||
|
|
||||||
|
Что такое шаблоны кодогенерации и какие проблемы они решают.
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
Каждый новый модуль в проекте — компонент, стор, бизнес-модуль — требует однотипной структуры файлов и boilerplate-кода. Ручное создание приводит к трём проблемам:
|
||||||
|
|
||||||
|
- **Расхождения.** Разные разработчики создают модули по-разному: забывают `index.ts`, называют типы не по канону, пропускают стили.
|
||||||
|
- **Время.** Создание одного компонента с типами, стилями и экспортом — 5–10 минут рутины. За спринт набегают часы.
|
||||||
|
- **Ошибки копипасты.** Копирование существующего модуля и переименование — источник опечаток и забытых ссылок.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Шаблоны кодогенерации — это папки с файлами-заготовками в `.templates/`. Вместо ручного создания файлов разработчик вызывает генератор, указывает имя — и получает готовый модуль со всей структурой, именами и boilerplate, подставленными автоматически.
|
||||||
|
|
||||||
|
Что дают шаблоны:
|
||||||
|
|
||||||
|
- **Единообразие.** Все модули одного типа идентичны по структуре. Канон живёт в шаблоне, а не в памяти разработчика.
|
||||||
|
- **Скорость.** Генерация модуля — одна команда. Остальное время — на бизнес-логику.
|
||||||
|
- **Согласованность с архитектурой.** Шаблоны учитывают SLM: правильные слои, сегменты, экспорты. Отклонение от стайлгайда требует осознанного усилия, а не случайного упущения.
|
||||||
|
|
||||||
|
## Состав раздела
|
||||||
|
|
||||||
|
- [Настройка](/docs/applied/templates/templates-setup) — первичная установка: скачивание стандартного набора шаблонов в проект.
|
||||||
|
- [Создание шаблонов](/docs/applied/templates/templates-create) — структура файлов, синтаксис переменных, примеры.
|
||||||
|
- [Использование](/docs/applied/templates/templates-usage) — генерация через VS Code плагин и CLI.
|
||||||
44
canons/style-guide/applied/templates/templates-setup.md
Normal file
44
canons/style-guide/applied/templates/templates-setup.md
Normal 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.
|
||||||
45
canons/style-guide/applied/templates/templates-usage.md
Normal file
45
canons/style-guide/applied/templates/templates-usage.md
Normal 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` отдельно не добавляется.
|
||||||
88
canons/style-guide/applied/vscode.md
Normal file
88
canons/style-guide/applied/vscode.md
Normal 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` с общими для команды настройками.
|
||||||
109
canons/style-guide/basics/architecture/index.md
Normal file
109
canons/style-guide/basics/architecture/index.md
Normal 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.
|
||||||
|
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.
|
||||||
254
canons/style-guide/basics/architecture/layers.md
Normal file
254
canons/style-guide/basics/architecture/layers.md
Normal 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-состояния
|
||||||
289
canons/style-guide/basics/architecture/modules.md
Normal file
289
canons/style-guide/basics/architecture/modules.md
Normal 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/`
|
||||||
|
|
||||||
|
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||||
181
canons/style-guide/basics/architecture/segments.md
Normal file
181
canons/style-guide/basics/architecture/segments.md
Normal 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
|
||||||
|
```
|
||||||
153
canons/style-guide/basics/code-style.md
Normal file
153
canons/style-guide/basics/code-style.md
Normal 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 } };
|
||||||
|
```
|
||||||
134
canons/style-guide/basics/documentation.md
Normal file
134
canons/style-guide/basics/documentation.md
Normal 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
146
canons/style-guide/basics/naming.md
Normal file
146
canons/style-guide/basics/naming.md
Normal 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>;
|
||||||
|
```
|
||||||
42
canons/style-guide/basics/tech-stack.md
Normal file
42
canons/style-guide/basics/tech-stack.md
Normal 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` — шаблонизатор для создания слоёв и других файлов из шаблонов.
|
||||||
62
canons/style-guide/basics/typing.md
Normal file
62
canons/style-guide/basics/typing.md
Normal 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;
|
||||||
|
```
|
||||||
79
canons/style-guide/index.md
Normal file
79
canons/style-guide/index.md
Normal 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 | _(не заполнен)_ |
|
||||||
|
| Хуки | _(не заполнен)_ |
|
||||||
|
| Шрифты | _(не заполнен)_ |
|
||||||
|
| Локализация | _(не заполнен)_ |
|
||||||
2
canons/style-guide/slm-design/VERSION
Normal file
2
canons/style-guide/slm-design/VERSION
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
v0.1.5
|
||||||
|
2026-05-11T18:29:26.821Z
|
||||||
114
canons/style-guide/slm-design/architecture/index.md
Normal file
114
canons/style-guide/slm-design/architecture/index.md
Normal 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.
|
||||||
|
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.
|
||||||
254
canons/style-guide/slm-design/architecture/layers.md
Normal file
254
canons/style-guide/slm-design/architecture/layers.md
Normal 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-состояния
|
||||||
213
canons/style-guide/slm-design/architecture/modules.md
Normal file
213
canons/style-guide/slm-design/architecture/modules.md
Normal 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/`
|
||||||
|
|
||||||
|
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||||
235
canons/style-guide/slm-design/architecture/monorepo.md
Normal file
235
canons/style-guide/slm-design/architecture/monorepo.md
Normal 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/*`.
|
||||||
181
canons/style-guide/slm-design/architecture/segments.md
Normal file
181
canons/style-guide/slm-design/architecture/segments.md
Normal 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
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -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-модуля.
|
||||||
114
canons/style-guide/slm-design/examples/react/factory.md
Normal file
114
canons/style-guide/slm-design/examples/react/factory.md
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
8
canons/style-guide/workflow.md
Normal file
8
canons/style-guide/workflow.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: Подсказки
|
||||||
|
description: Короткие ответы на типовые вопросы и решения для спорных ситуаций.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Подсказки
|
||||||
|
|
||||||
|
Короткие ответы на типовые вопросы и решения для спорных ситуаций.
|
||||||
13035
docs/slm-design/.vitepress/cache/deps_temp_f6246001/chunk-BKAC4ZFU.js
vendored
Normal file
13035
docs/slm-design/.vitepress/cache/deps_temp_f6246001/chunk-BKAC4ZFU.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
docs/slm-design/.vitepress/cache/deps_temp_f6246001/chunk-BKAC4ZFU.js.map
vendored
Normal file
7
docs/slm-design/.vitepress/cache/deps_temp_f6246001/chunk-BKAC4ZFU.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
docs/slm-design/.vitepress/cache/deps_temp_f6246001/package.json
vendored
Normal file
3
docs/slm-design/.vitepress/cache/deps_temp_f6246001/package.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
4505
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vue_devtools-api.js
vendored
Normal file
4505
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vue_devtools-api.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vue_devtools-api.js.map
vendored
Normal file
7
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vue_devtools-api.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
9731
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vueuse_core.js
vendored
Normal file
9731
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vueuse_core.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vueuse_core.js.map
vendored
Normal file
7
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vitepress___@vueuse_core.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
347
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vue.js
vendored
Normal file
347
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vue.js
vendored
Normal 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
|
||||||
7
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vue.js.map
vendored
Normal file
7
docs/slm-design/.vitepress/cache/deps_temp_f6246001/vue.js.map
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [],
|
||||||
|
"sourcesContent": [],
|
||||||
|
"mappings": "",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
19
docs/slm-design/.vitepress/config.ts
Normal file
19
docs/slm-design/.vitepress/config.ts
Normal 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: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
66
docs/slm-design/docs.config.ts
Normal file
66
docs/slm-design/docs.config.ts
Normal 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
22
eslint.config.js
Normal 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
14
index.html
Normal 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
6230
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal 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
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
24
public/icons.svg
Normal 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
85
scripts/docs/prepare.ts
Normal 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} страниц`);
|
||||||
65
scripts/site/generate-artifacts.ts
Normal file
65
scripts/site/generate-artifacts.ts
Normal 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
465
src/App.css
Normal 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
290
src/App.tsx
Normal 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}>
|
||||||
|
Открыть ->
|
||||||
|
</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
BIN
src/assets/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal 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
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
67
src/config/docs.config.ts
Normal 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
83
src/index.css
Normal 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
10
src/main.tsx
Normal 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
Reference in New Issue
Block a user