Compare commits
11 Commits
8f49e8c48b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9da566ab7f | |||
| 89cc873c19 | |||
| 9a962f37b5 | |||
| bdb99ade62 | |||
| a53c5fc1b1 | |||
| 93f4b468c4 | |||
| 1a14df9366 | |||
| 07b349e678 | |||
| 53aa01199d | |||
| ab72c06fd5 | |||
| 7554ce58db |
@@ -7,6 +7,9 @@ docs/*/.vitepress/cache
|
||||
docs/*/.vitepress/dist
|
||||
docs/*/content
|
||||
public/slm-design
|
||||
public/figma-adaptive-standards
|
||||
public/template-sync-strategy
|
||||
public/agents
|
||||
*.log
|
||||
.DS_Store
|
||||
.env*
|
||||
|
||||
@@ -20,13 +20,7 @@ jobs:
|
||||
- name: Установка зависимостей
|
||||
run: npm ci
|
||||
|
||||
- name: Сборка документации SLM Design
|
||||
run: npm run docs:build:slm-design
|
||||
|
||||
- name: Генерация корневых артефактов
|
||||
run: npm run site:generate
|
||||
|
||||
- name: Сборка лендинга
|
||||
- name: Сборка артефактов проекта
|
||||
run: npm run build
|
||||
|
||||
docker:
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -13,10 +13,13 @@ dist-ssr
|
||||
*.local
|
||||
|
||||
# Generated docs
|
||||
docs/*/content/
|
||||
docs/*/.vitepress/cache/
|
||||
projects/*/docs/content/
|
||||
projects/*/docs/.vitepress/cache/
|
||||
public/llms.txt
|
||||
public/slm-design/
|
||||
public/nextjs-style-guide/
|
||||
public/figma-adaptive-standards/
|
||||
public/template-sync-strategy/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -21,8 +21,11 @@
|
||||
@figmaAdaptiveStandards path /figma-adaptive-standards /figma-adaptive-standards/*
|
||||
header @figmaAdaptiveStandards Link "</figma-adaptive-standards/llms.txt>; rel=\"llms\""
|
||||
|
||||
@templateSyncStrategy path /template-sync-strategy /template-sync-strategy/*
|
||||
header @templateSyncStrategy Link "</template-sync-strategy/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/*
|
||||
not path /slm-design /slm-design/* /nextjs-style-guide /nextjs-style-guide/* /react-style-guide /react-style-guide/* /figma-adaptive-standards /figma-adaptive-standards/* /template-sync-strategy /template-sync-strategy/*
|
||||
}
|
||||
header @root Link "</llms.txt>; rel=\"llms\""
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run docs:build:slm-design && npm run build
|
||||
RUN npm run build
|
||||
|
||||
FROM caddy:2-alpine
|
||||
|
||||
|
||||
67
README.md
67
README.md
@@ -8,27 +8,36 @@
|
||||
|
||||
- React/Vite-лендинг со списком документаций.
|
||||
- VitePress-сборка для `SLM Design`.
|
||||
- VitePress-сборка для `NextJS Style Guide`.
|
||||
- VitePress-сборка для `Figma Adaptive Standards`.
|
||||
- VitePress-сборка для `Template Sync Strategy`.
|
||||
- Корневой `llms.txt` как карта всех документаций.
|
||||
- Собственные `llms.txt` и `llms-full.txt` внутри каждой документации.
|
||||
- ZIP-архивы Markdown-контента для каждой документации.
|
||||
- Docker/Caddy-конфигурация для публикации статической сборки.
|
||||
- Gitea CI/CD для ветки `master`.
|
||||
|
||||
## Документации
|
||||
|
||||
- `SLM Design` — архитектура frontend-приложений через слои, модули, публичные API и DI через фабрики.
|
||||
- `NextJS Style Guide` — будущие правила организации Next.js-приложений.
|
||||
- `NextJS Style Guide` — практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript.
|
||||
- `React Style Guide` — будущие правила написания React-кода.
|
||||
- `Figma Adaptive Standards` — будущие стандарты подготовки адаптивных макетов в Figma.
|
||||
- `Figma Adaptive Standards` — стандарты подготовки адаптивных макетов в Figma.
|
||||
- `Template Sync Strategy` — стратегия создания проектов от шаблона и долгосрочного обновления приложений через Git.
|
||||
|
||||
## Структура
|
||||
|
||||
```text
|
||||
canons/ исходные материалы и черновики
|
||||
docs/slm-design/ VitePress-сайт SLM Design
|
||||
scripts/docs/ подготовка контента для документаций
|
||||
scripts/site/ генерация корневых артефактов сайта
|
||||
src/ React-лендинг
|
||||
public/ статические файлы и сгенерированные документации
|
||||
build.ts сборка всего репозитория
|
||||
projects/<slug>/build.ts сборка конкретного проекта
|
||||
projects/<slug>/canons/ исходные Markdown-материалы проекта
|
||||
projects/<slug>/docs/ VitePress-конфигурация проекта
|
||||
projects/<slug>/scripts/ уникальные вспомогательные скрипты проекта
|
||||
projects/_shared/lib/ общие библиотечные функции сборки
|
||||
projects/_shared/docs/ общая VitePress-тема
|
||||
src/ React-лендинг
|
||||
public/ статические файлы и сгенерированные документации
|
||||
dist/ итоговая статическая сборка
|
||||
```
|
||||
|
||||
## Команды
|
||||
@@ -39,19 +48,39 @@ npm run dev
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run docs:build:slm-design
|
||||
npm run site:generate
|
||||
npm run build
|
||||
```
|
||||
|
||||
Основные скрипты:
|
||||
|
||||
- `npm run dev` — запускает Vite dev server.
|
||||
- `npm run docs:build:slm-design` — подготавливает и собирает VitePress-документацию SLM Design.
|
||||
- `npm run site:generate` — генерирует корневой `public/llms.txt` из `src/config/docs.config.ts` и хардкод-секций.
|
||||
- `npm run build` — генерирует корневые артефакты и собирает лендинг.
|
||||
- `npm run build` — одной командой собирает проектные документации, ZIP-архивы, корневой `llms.txt`, агентов и лендинг.
|
||||
- `npm run build:slm-design` — собирает только проект `SLM Design`.
|
||||
- `npm run build:nextjs-style-guide` — собирает только проект `NextJS Style Guide`.
|
||||
- `npm run build:figma-adaptive-standards` — собирает только проект `Figma Adaptive Standards`.
|
||||
- `npm run build:template-sync-strategy` — собирает только проект `Template Sync Strategy`.
|
||||
- `npm run app:build` — собирает React/Vite-лендинг без проектных артефактов.
|
||||
- `npm run lint` — запускает ESLint.
|
||||
|
||||
## Проекты
|
||||
|
||||
Каждая документация живёт в собственной папке `projects/<slug>` и сама владеет исходниками, конфигами и сборкой.
|
||||
|
||||
```text
|
||||
projects/slm-design/
|
||||
build.ts
|
||||
project.config.ts
|
||||
canons/
|
||||
docs/
|
||||
docs.config.ts
|
||||
.vitepress/
|
||||
scripts/
|
||||
```
|
||||
|
||||
Общая команда `npm run build` запускает корневой `build.ts`. Он последовательно вызывает `projects/<slug>/build.ts`, затем собирает общие артефакты репозитория.
|
||||
|
||||
Если скрипт является библиотечной функцией сборки, он лежит в `projects/_shared/lib`. Если скрипт уникален для проекта, он лежит в `projects/<slug>/scripts`.
|
||||
|
||||
## LLM-артефакты
|
||||
|
||||
Корневой файл:
|
||||
@@ -67,6 +96,16 @@ npm run build
|
||||
```text
|
||||
/slm-design/llms.txt
|
||||
/slm-design/llms-full.txt
|
||||
/nextjs-style-guide/llms.txt
|
||||
/nextjs-style-guide/llms-full.txt
|
||||
/figma-adaptive-standards/llms.txt
|
||||
/figma-adaptive-standards/llms-full.txt
|
||||
/template-sync-strategy/llms.txt
|
||||
/template-sync-strategy/llms-full.txt
|
||||
/slm-design/slm-design.zip
|
||||
/nextjs-style-guide/nextjs-style-guide.zip
|
||||
/figma-adaptive-standards/figma-adaptive-standards.zip
|
||||
/template-sync-strategy/template-sync-strategy.zip
|
||||
```
|
||||
|
||||
Корневой `llms-full.txt` намеренно не создаётся. Полные bundles остаются внутри конкретных документаций.
|
||||
@@ -79,6 +118,7 @@ npm run build
|
||||
- `/nextjs-style-guide/*` → `/nextjs-style-guide/llms.txt`
|
||||
- `/react-style-guide/*` → `/react-style-guide/llms.txt`
|
||||
- `/figma-adaptive-standards/*` → `/figma-adaptive-standards/llms.txt`
|
||||
- `/template-sync-strategy/*` → `/template-sync-strategy/llms.txt`
|
||||
- остальные пути → `/llms.txt`
|
||||
|
||||
Редиректов `llms.txt` в корень нет.
|
||||
@@ -94,7 +134,6 @@ docker build -t all-docs:test .
|
||||
Docker-сборка выполняет:
|
||||
|
||||
```bash
|
||||
npm run docs:build:slm-design
|
||||
npm run build
|
||||
```
|
||||
|
||||
|
||||
9
build.ts
Normal file
9
build.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { generateRootLlms } from './projects/_shared/lib/root-llms';
|
||||
import { run } from './projects/_shared/lib/run';
|
||||
|
||||
run('npm', ['run', 'build:slm-design']);
|
||||
run('npm', ['run', 'build:nextjs-style-guide']);
|
||||
run('npm', ['run', 'build:figma-adaptive-standards']);
|
||||
run('npm', ['run', 'build:template-sync-strategy']);
|
||||
generateRootLlms();
|
||||
run('npm', ['run', 'app:build']);
|
||||
@@ -1,254 +0,0 @@
|
||||
---
|
||||
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-состояния
|
||||
@@ -1,249 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
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-модуля.
|
||||
@@ -1,109 +0,0 @@
|
||||
---
|
||||
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.
|
||||
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.
|
||||
@@ -1,289 +0,0 @@
|
||||
---
|
||||
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/`
|
||||
|
||||
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||
@@ -1,181 +0,0 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
@@ -1,2 +0,0 @@
|
||||
v0.1.5
|
||||
2026-05-11T18:29:26.821Z
|
||||
@@ -1,114 +0,0 @@
|
||||
---
|
||||
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.
|
||||
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.
|
||||
@@ -1,254 +0,0 @@
|
||||
---
|
||||
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-состояния
|
||||
@@ -1,213 +0,0 @@
|
||||
---
|
||||
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/`
|
||||
|
||||
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||
@@ -1,235 +0,0 @@
|
||||
---
|
||||
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/*`.
|
||||
@@ -1,181 +0,0 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
@@ -1,249 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
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-модуля.
|
||||
@@ -1,114 +0,0 @@
|
||||
---
|
||||
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,
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -6,7 +6,12 @@ import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
globalIgnores([
|
||||
'dist',
|
||||
'projects/*/docs/content',
|
||||
'projects/*/docs/.vitepress/cache',
|
||||
'public',
|
||||
]),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
|
||||
252
index.html
252
index.html
@@ -5,10 +5,260 @@
|
||||
<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="Единое пространство для идей, черновиков и первых версий документаций" />
|
||||
<script>
|
||||
;(() => {
|
||||
const theme = localStorage.getItem('vitepress-theme-appearance') || localStorage.getItem('all-docs-theme') || 'auto'
|
||||
const normalizedTheme = theme === 'system' ? 'auto' : theme
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const resolvedTheme = normalizedTheme === 'auto' ? (prefersDark ? 'dark' : 'light') : normalizedTheme
|
||||
|
||||
document.documentElement.dataset.theme = normalizedTheme
|
||||
document.documentElement.classList.toggle('dark', resolvedTheme === 'dark')
|
||||
document.documentElement.style.colorScheme = resolvedTheme
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
.static-shell {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px;
|
||||
color: CanvasText;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.static-shell h1 {
|
||||
margin: 0 0 16px;
|
||||
font-size: clamp(32px, 6vw, 56px);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.static-shell p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.static-lead {
|
||||
max-width: 760px;
|
||||
color: color-mix(in srgb, CanvasText 72%, Canvas);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.static-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.static-docs {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
margin: 40px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.static-card {
|
||||
padding: 20px;
|
||||
border: 1px solid color-mix(in srgb, CanvasText 18%, Canvas);
|
||||
border-radius: 16px;
|
||||
background: color-mix(in srgb, CanvasText 4%, Canvas);
|
||||
}
|
||||
|
||||
.static-card h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.static-meta {
|
||||
margin-bottom: 8px;
|
||||
color: color-mix(in srgb, CanvasText 58%, Canvas);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.static-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.static-actions-list,
|
||||
.static-action-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 14px 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.static-action-list {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.static-action-list-nested {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.static-action-sections {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.static-action-section,
|
||||
.static-action-group {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.static-action-title {
|
||||
color: color-mix(in srgb, LinkText 80%, CanvasText);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.static-action-group-title {
|
||||
color: color-mix(in srgb, CanvasText 58%, Canvas);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.static-shell a {
|
||||
color: LinkText;
|
||||
}
|
||||
|
||||
.static-footer {
|
||||
color: color-mix(in srgb, CanvasText 58%, Canvas);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
<title>Документация</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="root">
|
||||
<main class="static-shell" aria-labelledby="static-title">
|
||||
<header>
|
||||
<h1 id="static-title">Документация</h1>
|
||||
<p class="static-lead">
|
||||
Единое пространство для идей, черновиков и первых версий документаций,
|
||||
которые ещё формируются и постепенно становятся самостоятельными материалами.
|
||||
</p>
|
||||
<div class="static-actions">
|
||||
<a href="https://gromlab.ru/gromov/docs">Репозиторий</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- STATIC_DOCS_START -->
|
||||
<section aria-labelledby="static-docs-title">
|
||||
<h2 id="static-docs-title">Список документаций</h2>
|
||||
<ul class="static-docs">
|
||||
<li class="static-card">
|
||||
<article>
|
||||
<div class="static-meta">Архитектура · Доступно</div>
|
||||
<h2><a href="/slm-design/">SLM Design</a></h2>
|
||||
<p>Архитектура frontend-приложений, где слои задают направление зависимостей, модули становятся границами ответственности, а явный DI через фабрики удерживает домены изолированными и предсказуемыми.</p>
|
||||
<ul class="static-actions-list">
|
||||
<li>
|
||||
<span class="static-action-title">Открыть</span>
|
||||
<ul class="static-action-list static-action-list-nested">
|
||||
<li>
|
||||
<span class="static-action-group-title">Читать</span>
|
||||
<ul class="static-action-list"><li><a href="/slm-design/" target="_blank" rel="noopener noreferrer">SLM Документация</a></li></ul>
|
||||
</li>
|
||||
<li>
|
||||
<span class="static-action-group-title">Skill для CLI-агентов</span>
|
||||
<ul class="static-action-list"><li><a href="/slm-design/skill/slm-design/SKILL.md" target="_blank" rel="noopener noreferrer">slm-design/SKILL.md</a></li></ul>
|
||||
</li>
|
||||
<li>
|
||||
<span class="static-action-group-title">AI агентам</span>
|
||||
<ul class="static-action-list"><li><a href="/slm-design/llms.txt" target="_blank" rel="noopener noreferrer">llms.txt</a></li><li><a href="/slm-design/llms-full.txt" target="_blank" rel="noopener noreferrer">llms-full.txt</a></li></ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<span class="static-action-title">Скачать</span>
|
||||
<ul class="static-action-list static-action-list-nested">
|
||||
<li>
|
||||
<span class="static-action-group-title">Документация MD</span>
|
||||
<ul class="static-action-list"><li><a href="/slm-design/slm-design.zip" download>slm-design.zip</a></li></ul>
|
||||
</li>
|
||||
<li>
|
||||
<span class="static-action-group-title">Skills (Claude code / OpenCode)</span>
|
||||
<ul class="static-action-list"><li><a href="/slm-design/skill/slm-design.skill.zip" download>slm-design.skill.zip</a></li></ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</li>
|
||||
<li class="static-card">
|
||||
<article>
|
||||
<div class="static-meta">Стайлгайд · Доступно</div>
|
||||
<h2><a href="/nextjs-style-guide/">NextJS Style Guide</a></h2>
|
||||
<p>Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript.</p>
|
||||
<ul class="static-actions-list">
|
||||
<li>
|
||||
<span class="static-action-title">AI</span>
|
||||
<ul class="static-action-list"><li><a href="/nextjs-style-guide/llms.txt" target="_blank" rel="noopener noreferrer">llms.txt</a></li><li><a href="/nextjs-style-guide/llms-full.txt" target="_blank" rel="noopener noreferrer">llms-full.txt</a></li></ul>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</li>
|
||||
<li class="static-card">
|
||||
<article>
|
||||
<div class="static-meta">Стайлгайд · Скоро</div>
|
||||
<h2>React Style Guide</h2>
|
||||
<p>Практический стайлгайд для разработки frontend-приложений на React и TypeScript.</p>
|
||||
<ul class="static-actions-list">
|
||||
<li>
|
||||
<span class="static-action-title">AI</span>
|
||||
<ul class="static-action-list"><li><a href="/react-style-guide/llms.txt" target="_blank" rel="noopener noreferrer">llms.txt</a></li><li><a href="/react-style-guide/llms-full.txt" target="_blank" rel="noopener noreferrer">llms-full.txt</a></li></ul>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</li>
|
||||
<li class="static-card">
|
||||
<article>
|
||||
<div class="static-meta">Макеты · Доступно</div>
|
||||
<h2><a href="/figma-adaptive-standards/">Figma Adaptive Standards</a></h2>
|
||||
<p>Стандарты и требования к подготовке адаптивных макетов в Figma: брейкпоинты, ресайз в диапазоне, Auto Layout/Constraints, компоненты, сетка, типографика, состояния UI, A11y и передача в разработку.</p>
|
||||
<ul class="static-actions-list">
|
||||
<li>
|
||||
<span class="static-action-title">AI</span>
|
||||
<ul class="static-action-list"><li><a href="/figma-adaptive-standards/llms.txt" target="_blank" rel="noopener noreferrer">llms.txt</a></li><li><a href="/figma-adaptive-standards/llms-full.txt" target="_blank" rel="noopener noreferrer">llms-full.txt</a></li></ul>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</li>
|
||||
<li class="static-card">
|
||||
<article>
|
||||
<div class="static-meta">Стратегия · Доступно</div>
|
||||
<h2><a href="/template-sync-strategy/">Template Sync Strategy</a></h2>
|
||||
<p>Стратегия как поддерживать проекты на общей шаблонной базе: отделять изменения шаблона от бизнес-кода и проводить обновления через контролируемый merge-процесс.</p>
|
||||
<ul class="static-actions-list">
|
||||
<li>
|
||||
<span class="static-action-title">AI</span>
|
||||
<ul class="static-action-list"><li><a href="/template-sync-strategy/llms.txt" target="_blank" rel="noopener noreferrer">llms.txt</a></li><li><a href="/template-sync-strategy/llms-full.txt" target="_blank" rel="noopener noreferrer">llms-full.txt</a></li></ul>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<!-- STATIC_DOCS_END -->
|
||||
|
||||
<footer class="static-footer">
|
||||
Автор документации: <a href="https://gromlab.ru/gromov">Сергей Громов</a>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
337
package-lock.json
generated
337
package-lock.json
generated
@@ -8,6 +8,9 @@
|
||||
"name": "all-docs",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@mantine/core": "^9.2.1",
|
||||
"@mantine/hooks": "^9.2.1",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6"
|
||||
},
|
||||
@@ -21,6 +24,7 @@
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.6.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
@@ -190,7 +194,6 @@
|
||||
"integrity": "sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.52.1",
|
||||
"@algolia/requester-browser-xhr": "5.52.1",
|
||||
@@ -313,7 +316,6 @@
|
||||
"version": "7.29.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -590,17 +592,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@docsearch/js/node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
@@ -1256,6 +1247,59 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.5",
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.27.19",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz",
|
||||
"integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.8",
|
||||
"@floating-ui/utils": "^0.2.11",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
|
||||
"integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.2",
|
||||
"dev": true,
|
||||
@@ -1369,6 +1413,46 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/core": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-9.2.1.tgz",
|
||||
"integrity": "sha512-CicPg9i2dM2pGp1jj+dMiR/63OFDsPjgJke4v5+0nbfJ+C7gn4C+7ltrp4RIETDMZHcj0fFuDRG0qtbiyBxvWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.19",
|
||||
"clsx": "^2.1.1",
|
||||
"react-number-format": "^5.4.5",
|
||||
"react-remove-scroll": "^2.7.2",
|
||||
"type-fest": "^5.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mantine/hooks": "9.2.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/hooks": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-9.2.1.tgz",
|
||||
"integrity": "sha512-IX/ztVG9eWmQTRsN7G8odyW4JckNvN8qv5A2ULzXyazjtAKLuaUpuMz0c6XhRp10J0g4bVfV3rhrTgWeImqxqg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@phosphor-icons/react": {
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz",
|
||||
"integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8",
|
||||
"react-dom": ">= 16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||
@@ -1938,24 +2022,14 @@
|
||||
"version": "24.12.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.14",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -2021,7 +2095,6 @@
|
||||
"version": "8.59.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
@@ -2501,7 +2574,6 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2540,7 +2612,6 @@
|
||||
"integrity": "sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@algolia/abtesting": "1.18.1",
|
||||
"@algolia/client-abtesting": "5.52.1",
|
||||
@@ -2663,7 +2734,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@@ -2783,6 +2853,15 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2857,7 +2936,7 @@
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
@@ -2905,6 +2984,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node-es": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||
@@ -3015,7 +3100,6 @@
|
||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3374,7 +3458,6 @@
|
||||
"integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tabbable": "^6.4.0"
|
||||
}
|
||||
@@ -3421,6 +3504,15 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
|
||||
@@ -3805,20 +3897,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
@@ -3862,6 +3940,13 @@
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it-task-lists": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
|
||||
"integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/markdown-it/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
@@ -4685,7 +4770,6 @@
|
||||
"version": "4.0.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4786,7 +4870,6 @@
|
||||
"node_modules/react": {
|
||||
"version": "19.2.6",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -4801,6 +4884,16 @@
|
||||
"react": "^19.2.6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-number-format": {
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.5.tgz",
|
||||
"integrity": "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
@@ -4811,6 +4904,75 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
|
||||
"integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.7",
|
||||
"react-style-singleton": "^2.2.3",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.3",
|
||||
"use-sidecar": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll-bar": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
|
||||
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-style-singleton": "^2.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-nonce": "^1.0.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
|
||||
@@ -5198,9 +5360,20 @@
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
|
||||
"integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tagged-tag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"dev": true,
|
||||
@@ -5256,6 +5429,12 @@
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
@@ -5720,13 +5899,27 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz",
|
||||
"integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"dependencies": {
|
||||
"tagged-tag": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -5915,6 +6108,49 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-callback-ref": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-node-es": "^1.1.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vfile": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||
@@ -5951,7 +6187,6 @@
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@@ -6083,7 +6318,6 @@
|
||||
"integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.34",
|
||||
"@vue/compiler-sfc": "3.5.34",
|
||||
@@ -6199,7 +6433,6 @@
|
||||
"version": "4.4.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
17
package.json
17
package.json
@@ -4,15 +4,21 @@
|
||||
"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",
|
||||
"dev": "npm run prepare:static-index && vite",
|
||||
"build": "tsx build.ts",
|
||||
"app:build": "npm run prepare:static-index && tsc -b && vite build",
|
||||
"prepare:static-index": "tsx scripts/prepare-static-index.ts",
|
||||
"build:slm-design": "tsx projects/slm-design/build.ts",
|
||||
"build:nextjs-style-guide": "tsx projects/nextjs-style-guide/build.ts",
|
||||
"build:figma-adaptive-standards": "tsx projects/figma-adaptive-standards/build.ts",
|
||||
"build:template-sync-strategy": "tsx projects/template-sync-strategy/build.ts",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/core": "^9.2.1",
|
||||
"@mantine/hooks": "^9.2.1",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6"
|
||||
},
|
||||
@@ -26,6 +32,7 @@
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.6.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
|
||||
174
projects/_shared/docs/vitepress/HomeLink.vue
Normal file
174
projects/_shared/docs/vitepress/HomeLink.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
variant?: 'back' | 'repo' | 'screen'
|
||||
}>(), {
|
||||
variant: 'back',
|
||||
})
|
||||
|
||||
const repositoryUrl = 'https://gromlab.ru/gromov/docs'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
v-if="variant === 'back'"
|
||||
class="docsNavLink docsBackLink"
|
||||
href="/"
|
||||
target="_self"
|
||||
aria-label="Вернуться к списку документаций"
|
||||
>
|
||||
<span class="docsBackIcon" aria-hidden="true">←</span>
|
||||
<span>К списку документаций</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="variant === 'repo'"
|
||||
class="docsRepoLink"
|
||||
:href="repositoryUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Открыть репозиторий"
|
||||
title="Репозиторий"
|
||||
>
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="M12 .5a12 12 0 0 0-3.8 23.38c.6.12.83-.26.83-.57l-.02-2.04c-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.01-.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 .5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<nav v-else class="docsNavLinks" aria-label="Навигация документации">
|
||||
<a
|
||||
class="docsNavLink"
|
||||
href="/"
|
||||
target="_self"
|
||||
aria-label="Вернуться к списку документаций"
|
||||
>
|
||||
<span class="docsBackIcon" aria-hidden="true">←</span>
|
||||
<span>К списку документаций</span>
|
||||
</a>
|
||||
<a
|
||||
class="docsNavLink"
|
||||
:href="repositoryUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Открыть репозиторий документации"
|
||||
>
|
||||
<svg class="docsScreenRepoIcon" aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="M12 .5a12 12 0 0 0-3.8 23.38c.6.12.83-.26.83-.57l-.02-2.04c-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.01-.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 .5Z" />
|
||||
</svg>
|
||||
Репозиторий
|
||||
</a>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.docsBackLink {
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.docsNavLinks {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.docsNavLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: var(--vp-nav-height);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.docsBackIcon {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 15px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.docsNavLink:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.docsNavLink:hover .docsBackIcon {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.docsNavLink:focus-visible {
|
||||
border-radius: 4px;
|
||||
outline: 2px solid var(--vp-c-brand-1);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.docsRepoLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: var(--vp-nav-height);
|
||||
margin-left: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.docsRepoLink:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.docsRepoLink:focus-visible {
|
||||
border-radius: 6px;
|
||||
outline: 2px solid var(--vp-c-brand-1);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.docsRepoLink svg,
|
||||
.docsScreenRepoIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.docsNavLinks {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
padding: 8px 0 12px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.docsNavLinks .docsNavLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.docsScreenRepoIcon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.docsBackLink,
|
||||
.docsRepoLink {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.docsNavLinks {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.docsBackLink {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15
projects/_shared/docs/vitepress/theme.ts
Normal file
15
projects/_shared/docs/vitepress/theme.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { h } from 'vue';
|
||||
import DefaultTheme from 'vitepress/theme';
|
||||
import type { Theme } from 'vitepress';
|
||||
import HomeLink from './HomeLink.vue';
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout() {
|
||||
return h(DefaultTheme.Layout, null, {
|
||||
'nav-bar-content-before': () => h(HomeLink, { variant: 'back' }),
|
||||
'nav-bar-content-after': () => h(HomeLink, { variant: 'repo' }),
|
||||
'nav-screen-content-before': () => h(HomeLink, { variant: 'screen' }),
|
||||
});
|
||||
},
|
||||
} satisfies Theme;
|
||||
18
projects/_shared/docs/vitepress/themeHead.ts
Normal file
18
projects/_shared/docs/vitepress/themeHead.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { HeadConfig } from 'vitepress';
|
||||
|
||||
export const themeSyncHead: HeadConfig[] = [
|
||||
[
|
||||
'script',
|
||||
{ id: 'sync-docs-theme' },
|
||||
`;(() => {
|
||||
const theme = localStorage.getItem('vitepress-theme-appearance')
|
||||
if (theme) return
|
||||
|
||||
const legacyTheme = localStorage.getItem('all-docs-theme')
|
||||
if (!legacyTheme) return
|
||||
|
||||
localStorage.setItem('vitepress-theme-appearance', legacyTheme === 'system' ? 'auto' : legacyTheme)
|
||||
localStorage.removeItem('all-docs-theme')
|
||||
})()`,
|
||||
],
|
||||
];
|
||||
112
projects/_shared/lib/prepare-docs.ts
Normal file
112
projects/_shared/lib/prepare-docs.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
type Page = {
|
||||
source: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
type RouteRewrite = {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
type DocsConfig = {
|
||||
mounts: Page[];
|
||||
routeRewrites?: RouteRewrite[];
|
||||
};
|
||||
|
||||
type ProjectConfig = {
|
||||
slug: string;
|
||||
docsDir: string;
|
||||
};
|
||||
|
||||
const MD_LINK_RE = /\]\((?!#|[a-z][a-z0-9+.-]*:|\/\/)([^)\s]+)(#[^)]*)?\)/gi;
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
async function loadDocsConfig(configPath: string) {
|
||||
return (await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`)) as DocsConfig;
|
||||
}
|
||||
|
||||
export async function prepareDocs(projectDir: string, config: ProjectConfig) {
|
||||
const docsDir = path.join(projectDir, config.docsDir);
|
||||
const contentDir = path.join(docsDir, 'content');
|
||||
const docsConfig = await loadDocsConfig(path.join(docsDir, 'docs.config.ts'));
|
||||
const targetBySource = new Map(
|
||||
docsConfig.mounts.map((page) => [normalizePath(path.resolve(projectDir, page.source)), normalizePath(page.target)]),
|
||||
);
|
||||
const routeRewrites = [...(docsConfig.routeRewrites ?? [])].sort((a, b) => b.from.length - a.from.length);
|
||||
|
||||
function applyRouteRewrites(route: string) {
|
||||
for (const rewrite of routeRewrites) {
|
||||
if (route === rewrite.from || route.startsWith(`${rewrite.from}/`) || route.startsWith(`${rewrite.from}#`)) {
|
||||
return `${rewrite.to}${route.slice(rewrite.from.length)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatDocsRoute(route: string) {
|
||||
const rewritten = applyRouteRewrites(route);
|
||||
if (rewritten) return rewritten;
|
||||
if (route === '/docs') return '/';
|
||||
if (route.startsWith('/docs/')) return route.slice('/docs'.length);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatRelativeRoute(hrefPath: string, sourceDir: string) {
|
||||
const sourcePath = normalizePath(path.relative(projectDir, path.resolve(sourceDir, hrefPath)));
|
||||
if (sourcePath.startsWith('canons/')) return formatDocsRoute(`/docs/${sourcePath.slice('canons/'.length)}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function transformMarkdownLinks(content: string, page: Page) {
|
||||
const sourceDir = path.dirname(path.resolve(projectDir, page.source));
|
||||
|
||||
return content.replace(MD_LINK_RE, (match, href: string, hash = '') => {
|
||||
const [hrefPath, query = ''] = href.split('?');
|
||||
const queryPart = query ? `?${query}` : '';
|
||||
|
||||
if (hrefPath.startsWith('/')) {
|
||||
const route = formatDocsRoute(hrefPath) ?? applyRouteRewrites(hrefPath);
|
||||
return route ? `](${route}${queryPart}${hash})` : match;
|
||||
}
|
||||
|
||||
if (!hrefPath.endsWith('.md')) {
|
||||
const route = formatRelativeRoute(hrefPath, sourceDir);
|
||||
return route ? `](${route}${queryPart}${hash})` : match;
|
||||
}
|
||||
|
||||
const target = targetBySource.get(normalizePath(path.resolve(sourceDir, hrefPath)));
|
||||
if (!target) return match;
|
||||
|
||||
return `](${formatRelativeMarkdownPath(page.target, `${target}${queryPart}`)}${hash})`;
|
||||
});
|
||||
}
|
||||
|
||||
fs.rmSync(contentDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(contentDir, { recursive: true });
|
||||
|
||||
for (const page of docsConfig.mounts) {
|
||||
const sourcePath = path.resolve(projectDir, page.source);
|
||||
const targetPath = path.join(contentDir, page.target);
|
||||
|
||||
if (!fs.existsSync(sourcePath)) throw new Error(`Не найден канон: ${sourcePath}`);
|
||||
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.writeFileSync(targetPath, transformMarkdownLinks(fs.readFileSync(sourcePath, 'utf8'), page), 'utf8');
|
||||
console.log(`${page.target} -> ${path.relative(projectDir, sourcePath)}`);
|
||||
}
|
||||
|
||||
console.log(`Подготовлен VitePress content для ${config.slug}: ${docsConfig.mounts.length} страниц`);
|
||||
}
|
||||
51
projects/_shared/lib/root-llms.ts
Normal file
51
projects/_shared/lib/root-llms.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { docs } from '../../../src/config/docs.config';
|
||||
|
||||
const siteTitle = 'Документация';
|
||||
const siteDescription = 'Единое пространство для идей, черновиков и первых версий документаций, которые ещё формируются и постепенно становятся самостоятельными материалами.';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function generateRootLlms(rootDir = process.cwd()) {
|
||||
const publicDir = path.join(rootDir, 'public');
|
||||
const llmsPath = path.join(publicDir, 'llms.txt');
|
||||
const content = [
|
||||
`# ${siteTitle}`,
|
||||
'',
|
||||
`> ${siteDescription}`,
|
||||
'',
|
||||
'Этот файл является корневой картой документаций. Для работы с конкретным направлением используйте его собственный `llms.txt`.',
|
||||
'',
|
||||
'## Documentation',
|
||||
'',
|
||||
...docs
|
||||
.map((doc) => {
|
||||
const link = findDocLink(doc, 'llms.txt');
|
||||
return link ? formatMarkdownLink(doc.title, link.href, doc.description) : undefined;
|
||||
})
|
||||
.filter(Boolean),
|
||||
'',
|
||||
'## Optional',
|
||||
'',
|
||||
...docs
|
||||
.map((doc) => {
|
||||
const link = findDocLink(doc, 'llms-full.txt');
|
||||
return link ? formatMarkdownLink(`${doc.title} full`, link.href, `Полный bundle документации: ${doc.label.toLowerCase()}.`) : undefined;
|
||||
})
|
||||
.filter(Boolean),
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
fs.writeFileSync(llmsPath, content, 'utf8');
|
||||
|
||||
console.log(`Сгенерирован ${path.relative(rootDir, llmsPath)}`);
|
||||
}
|
||||
12
projects/_shared/lib/run.ts
Normal file
12
projects/_shared/lib/run.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
export function run(command: string, args: string[], cwd = process.cwd()) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) throw new Error(`Command failed: ${[command, ...args].join(' ')}`);
|
||||
}
|
||||
136
projects/_shared/lib/zip.ts
Normal file
136
projects/_shared/lib/zip.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
type ZipEntry = {
|
||||
name: string;
|
||||
content: Buffer;
|
||||
};
|
||||
|
||||
function collectFiles(dir: string, baseDir = dir, archiveRoot = path.basename(dir)): ZipEntry[] {
|
||||
return fs
|
||||
.readdirSync(dir, { withFileTypes: true })
|
||||
.flatMap((entry) => {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) return collectFiles(entryPath, baseDir, archiveRoot);
|
||||
|
||||
const relativePath = path.relative(baseDir, entryPath).split(path.sep).join('/');
|
||||
|
||||
return [
|
||||
{
|
||||
name: archiveRoot ? `${archiveRoot}/${relativePath}` : relativePath,
|
||||
content: fs.readFileSync(entryPath),
|
||||
},
|
||||
];
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
function createCrc32Table() {
|
||||
return Array.from({ length: 256 }, (_, index) => {
|
||||
let value = index;
|
||||
|
||||
for (let bit = 0; bit < 8; bit += 1) {
|
||||
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
||||
}
|
||||
|
||||
return value >>> 0;
|
||||
});
|
||||
}
|
||||
|
||||
const crc32Table = createCrc32Table();
|
||||
|
||||
function crc32(buffer: Buffer) {
|
||||
let crc = 0xffffffff;
|
||||
|
||||
for (const byte of buffer) {
|
||||
crc = crc32Table[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
||||
}
|
||||
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function writeUInt16(value: number) {
|
||||
const buffer = Buffer.alloc(2);
|
||||
buffer.writeUInt16LE(value, 0);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function writeUInt32(value: number) {
|
||||
const buffer = Buffer.alloc(4);
|
||||
buffer.writeUInt32LE(value >>> 0, 0);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function createZip(entries: ZipEntry[]) {
|
||||
const localParts: Buffer[] = [];
|
||||
const centralParts: Buffer[] = [];
|
||||
const dosTime = 0;
|
||||
const dosDate = ((2026 - 1980) << 9) | (1 << 5) | 1;
|
||||
let offset = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
const fileName = Buffer.from(entry.name, 'utf8');
|
||||
const checksum = crc32(entry.content);
|
||||
const size = entry.content.length;
|
||||
|
||||
const localHeader = Buffer.concat([
|
||||
writeUInt32(0x04034b50),
|
||||
writeUInt16(20),
|
||||
writeUInt16(0),
|
||||
writeUInt16(0),
|
||||
writeUInt16(dosTime),
|
||||
writeUInt16(dosDate),
|
||||
writeUInt32(checksum),
|
||||
writeUInt32(size),
|
||||
writeUInt32(size),
|
||||
writeUInt16(fileName.length),
|
||||
writeUInt16(0),
|
||||
fileName,
|
||||
]);
|
||||
|
||||
localParts.push(localHeader, entry.content);
|
||||
|
||||
centralParts.push(Buffer.concat([
|
||||
writeUInt32(0x02014b50),
|
||||
writeUInt16(20),
|
||||
writeUInt16(20),
|
||||
writeUInt16(0),
|
||||
writeUInt16(0),
|
||||
writeUInt16(dosTime),
|
||||
writeUInt16(dosDate),
|
||||
writeUInt32(checksum),
|
||||
writeUInt32(size),
|
||||
writeUInt32(size),
|
||||
writeUInt16(fileName.length),
|
||||
writeUInt16(0),
|
||||
writeUInt16(0),
|
||||
writeUInt16(0),
|
||||
writeUInt16(0),
|
||||
writeUInt32(0),
|
||||
writeUInt32(offset),
|
||||
fileName,
|
||||
]));
|
||||
|
||||
offset += localHeader.length + size;
|
||||
}
|
||||
|
||||
const centralDirectory = Buffer.concat(centralParts);
|
||||
const endOfCentralDirectory = Buffer.concat([
|
||||
writeUInt32(0x06054b50),
|
||||
writeUInt16(0),
|
||||
writeUInt16(0),
|
||||
writeUInt16(entries.length),
|
||||
writeUInt16(entries.length),
|
||||
writeUInt32(centralDirectory.length),
|
||||
writeUInt32(offset),
|
||||
writeUInt16(0),
|
||||
]);
|
||||
|
||||
return Buffer.concat([...localParts, centralDirectory, endOfCentralDirectory]);
|
||||
}
|
||||
|
||||
export function writeZipFromDirectory(sourceDir: string, zipPath: string, archiveRoot = path.basename(sourceDir)) {
|
||||
fs.mkdirSync(path.dirname(zipPath), { recursive: true });
|
||||
fs.writeFileSync(zipPath, createZip(collectFiles(sourceDir, sourceDir, archiveRoot)));
|
||||
}
|
||||
20
projects/figma-adaptive-standards/build.ts
Normal file
20
projects/figma-adaptive-standards/build.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { prepareDocs } from '../_shared/lib/prepare-docs';
|
||||
import { run } from '../_shared/lib/run';
|
||||
import { writeZipFromDirectory } from '../_shared/lib/zip';
|
||||
import config from './project.config';
|
||||
|
||||
const projectDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(projectDir, '../..');
|
||||
const docsDir = path.join(projectDir, config.docsDir);
|
||||
|
||||
await prepareDocs(projectDir, config);
|
||||
run('npx', ['vitepress', 'build', docsDir], rootDir);
|
||||
|
||||
if (config.archive) {
|
||||
const zipPath = path.join(rootDir, 'public', config.slug, `${config.slug}.zip`);
|
||||
writeZipFromDirectory(path.join(docsDir, 'content'), zipPath, config.slug);
|
||||
console.log(`Собран ${path.relative(rootDir, zipPath)}`);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
# Чеклист приёмки макета (Figma)
|
||||
|
||||
> Примечание: в этом чеклисте «обоснованно» означает: по каждому «выбору» (например: `Hug/Fill/Fixed`, wrap/truncate для текста, `Fill/Fit`, `Stretch/Center`) понятно **что выбрано**, **зачем** и **как это ведёт себя** на min/max диапазона и при длинном контенте. Если без пояснения можно понять по-разному — добавлена аннотация/комментарий.
|
||||
|
||||
## 0. Структура файла и нейминг
|
||||
- [ ] В файле есть рабочие страницы: Cover / Overview, Design / UI, Components, Styles / Tokens, Responsive / Breakpoints, Prototype (Archive / Old — при необходимости)
|
||||
- [ ] Экраны названы по единому правилу (например: `Feature / Screen — Breakpoint`)
|
||||
- [ ] Компоненты названы семантически и единообразно (без «Button 1 / Button 2»)
|
||||
- [ ] На странице `Responsive / Breakpoints` описаны правила перестроения для сложных зон (колонки, переносы, скрытие/появление элементов)
|
||||
|
||||
|
||||
|
||||
## 1. Брейкпоинты
|
||||
- [ ] Все якорные брейкпоинты созданы в виде отдельных фреймов
|
||||
- Mobile, Mobile Wide, Tablet, Laptop, Desktop (диапазоны соблюдены)
|
||||
- [ ] Границы диапазонов трактуются единообразно (например: `Mobile ≤ 479`, `Mobile Wide 480–767`, `Tablet 768–1023`, `Laptop 1024–1439`, `Desktop ≥ 1440`)
|
||||
- [ ] Фреймы логически и визуально связаны
|
||||
- [ ] Дополнительные фреймы есть только при изменении сетки, навигации или структуры контента
|
||||
|
||||
|
||||
|
||||
## 2. Адаптивность и ресайз
|
||||
- [ ] Ресайз фрейма вручную до минимального значения диапазона
|
||||
- [ ] Ресайз фрейма до максимального значения диапазона
|
||||
- [ ] Проверены промежуточные значения (минимум 3–5 точек внутри диапазона)
|
||||
- [ ] Макет корректно меняется во всём диапазоне без создания дублирующих фреймов
|
||||
- [ ] Нет коллизий при ресайзе (наезды, обрезания, неожиданные перекрытия)
|
||||
- [ ] Карточки и блоки тянутся или сжимаются корректно
|
||||
- [ ] Текст переносится/обрезается по правилам и не выходит за пределы контейнеров
|
||||
- [ ] Навигация корректно адаптируется под ширину
|
||||
|
||||
|
||||
|
||||
## 3. Auto Layout и Constraints
|
||||
- [ ] Все контейнеры, списки, карточки, кнопки, хедеры, футеры, сайдбары используют Auto Layout
|
||||
- [ ] Padding задан со всех сторон через Auto Layout (без «ручных» отступов)
|
||||
- [ ] Spacing между элементами задан через Auto Layout
|
||||
- [ ] Режимы ресайза выбраны обоснованно (`Hug / Fill / Fixed`) для ключевых контейнеров и элементов
|
||||
- [ ] При необходимости заданы min/max (для предотвращения поломок на крайних ширинах)
|
||||
- [ ] Constraints заданы корректно (Left / Right / Top / Bottom / Center / Scale) там, где Auto Layout не покрывает сценарий
|
||||
- [ ] Нет ручного позиционирования или абсолютных размеров без причины
|
||||
|
||||
|
||||
|
||||
## 4. Сетка / Layout Grid
|
||||
- [ ] Layout Grid настроен на фреймах каждого брейкпоинта (колонки соответствуют принятому стандарту)
|
||||
- [ ] Margin и gutter заданы и согласованы с версткой
|
||||
- [ ] Поведение сетки выбрано обоснованно (Stretch/Center — по модели проекта)
|
||||
- [ ] Основные блоки выровнены по сетке и используют согласованный шаг отступов
|
||||
|
||||
|
||||
|
||||
## 5. Компоненты
|
||||
- [ ] Все повторяющиеся элементы оформлены как Components
|
||||
- [ ] Используются Variants вместо копий
|
||||
- [ ] Состояния компонентов показаны (минимум: Default, Hover, Active/Pressed, Disabled, Focus — если применимо)
|
||||
- [ ] Компоненты корректно тянутся и меняют высоту по контенту
|
||||
- [ ] Адаптивное поведение проверено во всех типовых контейнерах (экран, модалка, сайдбар, список/сетка)
|
||||
|
||||
|
||||
|
||||
## 6. Типографика
|
||||
- [ ] Используются Text Styles (`H1 / H2 / Body / Caption / Button` или принятый набор)
|
||||
- [ ] Используются только бесплатные шрифты (предпочтительно Google Fonts)
|
||||
- [ ] Есть коллекция Variables для типографики и Modes под брейкпоинты (например: Mobile/Mobile Wide/Tablet/Laptop/Desktop — как принято в проекте)
|
||||
- [ ] Text Styles используют переменные типографики (минимум размер и line-height; при необходимости letter-spacing)
|
||||
- [ ] На фреймах установлен корректный Mode для соответствующего брейкпоинта
|
||||
- [ ] Ограничение количества строк соблюдено там, где это критично
|
||||
- [ ] Правила для текста определены: перенос или обрезка (truncate), поведение при переполнении проверено
|
||||
- [ ] Проверены «экстремальные» тексты, включая одну очень длинную строку без пробелов
|
||||
|
||||
|
||||
|
||||
## 7. Цвета и стили
|
||||
- [ ] Все цвета через Color Styles
|
||||
- [ ] Семантические названия соблюдены (`Primary / Secondary`, `Text / Background / Border`, `Success / Error / Warning`)
|
||||
- [ ] Дополнительные стили (тени/обводки/радиусы), если используются, применены единообразно
|
||||
|
||||
|
||||
|
||||
## 8. Контент и состояния UI
|
||||
- [ ] Текст проверен на короткие / нормальные / длинные варианты
|
||||
- [ ] Проверка переполнения и переноса строк выполнена
|
||||
- [ ] Состояния UI присутствуют: Default, Hover, Active, Disabled, Focus, Loading, Empty, Error
|
||||
- [ ] Для форм/инпутов показаны состояния ошибок и валидации (включая сочетания вроде `Focus + Error`, если применимо)
|
||||
|
||||
|
||||
|
||||
## 9. Изображения и медиа
|
||||
- [ ] Fill / Fit задан корректно
|
||||
- [ ] Safe area для обрезки изображений соблюдена
|
||||
- [ ] Поддерживаются разные соотношения сторон (описано поведение при изменении ratio)
|
||||
- [ ] Форматы и размеры соответствуют требованиям проекта (если есть требования к экспорту)
|
||||
|
||||
|
||||
|
||||
## 10. Навигация
|
||||
- [ ] Desktop → горизонтальное меню
|
||||
- [ ] Tablet → сокращённое меню
|
||||
- [ ] Mobile → бургер / bottom bar
|
||||
- [ ] Описаны правила overflow/сокращения (что уходит в «ещё», что скрывается/переезжает)
|
||||
|
||||
|
||||
|
||||
## 11. Accessibility (A11y)
|
||||
- [ ] Контраст ≥ WCAG AA
|
||||
- [ ] Минимальный размер клика 44×44 px
|
||||
- [ ] Focus states проверены
|
||||
- [ ] Логичный tab order соблюден
|
||||
|
||||
|
||||
|
||||
## 12. Прототипирование
|
||||
- [ ] Основные пользовательские флоу представлены
|
||||
- [ ] Переходы между брейкпоинтами реализованы (если требуется)
|
||||
- [ ] Поведение при ресайзе проверено/продемонстрировано
|
||||
|
||||
|
||||
|
||||
## 13. Передача в разработку
|
||||
- [ ] Dev Mode включён
|
||||
- [ ] Все комментарии с логикой адаптивности оставлены
|
||||
- [ ] Брейкпоинты, поведение блоков, скрытие/появление элементов описаны
|
||||
- [ ] Ассеты к экспорту подготовлены (если требуется): корректные имена, форматы, настройки экспорта
|
||||
|
||||
|
||||
|
||||
## 14. Запрещено
|
||||
- [ ] Фиксированные ширины без причины
|
||||
- [ ] Дубли компонентов под каждый экран
|
||||
- [ ] Текст как вектор
|
||||
- [ ] Ручные отступы
|
||||
- [ ] Отсутствие Auto Layout
|
||||
|
||||
|
||||
|
||||
## ✅ Итоговое решение
|
||||
Макет считается **готовым к разработке**, если выполнены все пункты чеклиста и ресайз работает корректно во всех диапазонах.
|
||||
@@ -0,0 +1,473 @@
|
||||
# Требования к адаптивному дизайну в Figma (Полная версия)
|
||||
|
||||
Документ описывает стандарт подготовки **адаптивных** макетов в Figma так, чтобы они:
|
||||
|
||||
- корректно **ресайзились** в пределах заданных диапазонов ширин;
|
||||
- были **предсказуемы** для разработки (верстки);
|
||||
- минимизировали количество «ручных» правок и дублирования;
|
||||
- содержали достаточные пояснения по поведению UI.
|
||||
|
||||
## Кому адресовано
|
||||
- Дизайнерам интерфейсов
|
||||
- Team Lead / DesignOps
|
||||
- Разработчикам (для валидации качества и передачи в работу)
|
||||
|
||||
## Термины (коротко)
|
||||
- **Брейкпоинт** — якорная ширина и набор правил компоновки, по которым интерфейс меняет структуру.
|
||||
- **Диапазон** — интервал ширин, где **один** макет обязан работать корректно при ресайзе.
|
||||
- **Auto Layout** — основной механизм адаптивности в Figma для контейнеров и компонентов.
|
||||
- **Hug / Fill / Fixed** — режимы ресайза (по контенту / заполнять контейнер / фиксированно).
|
||||
- **Constraints** — привязки объектов внутри Frame (Left/Right/Top/Bottom/Center/Scale).
|
||||
- **«Обоснованно» (в контексте этого стандарта)** — по каждому «выбору» (например: `Hug/Fill/Fixed`, wrap/truncate для текста, `Fill/Fit`, `Stretch/Center`) должно быть понятно: **что именно выбрано**, **зачем** и **как это ведёт себя** на min/max диапазона и при длинном контенте. Если выбор может трактоваться по-разному — добавьте аннотацию/комментарий на странице `Responsive / Breakpoints`.
|
||||
|
||||
|
||||
|
||||
## 1. Структура файла Figma
|
||||
|
||||
### 1.1. Цель структуры
|
||||
Структура страниц нужна для:
|
||||
- быстрого поиска актуальных экранов и компонентов;
|
||||
- отделения финального UI от пояснений по адаптивности и интерактивности;
|
||||
- прозрачной передачи в разработку.
|
||||
|
||||
### 1.2. Рекомендуемая структура страниц
|
||||
- **Cover / Overview** — контекст: цель продукта, аудитория, платформы, контакты, ссылки, правила
|
||||
- **Design / UI** — финальные экраны (по брейкпоинтам или по флоу)
|
||||
- **Components** — библиотека компонентов и их состояния/варианты
|
||||
- **Styles / Tokens** — стили: цвет, типографика, эффекты, сетки (если принято)
|
||||
- **Responsive / Breakpoints** — правила поведения, схемы перестроения, примеры ресайза
|
||||
- **Prototype** — интерактивные сценарии и пользовательские флоу
|
||||
- **Archive / Old** — устаревшие версии (только для истории)
|
||||
|
||||
> Примечание: `Responsive / Breakpoints` и `Prototype` **не дублируют** финальные экраны один-в-один. Их задача — объяснить поведение, которое не очевидно из статичных макетов.
|
||||
|
||||
### 1.3. Правила нейминга (минимальный стандарт)
|
||||
- Страницы: `Design / UI`, `Components` и т.д. — одинаково во всех проектах.
|
||||
- Фреймы экранов: `Feature / Screen — Breakpoint` (например: `Catalog / List — Desktop`)
|
||||
- Компоненты: `ComponentName` + группа (`Button / Primary`, `Input / TextField`), семантика важнее внешнего вида.
|
||||
|
||||
### 1.4. Что считается «готовым набором» в файле
|
||||
В файле должны быть:
|
||||
- фреймы всех якорных брейкпоинтов;
|
||||
- компоненты и их состояния (через Variants);
|
||||
- стили (цвета/типографика) с семантическими именами;
|
||||
- пояснения по адаптивности и «порогам» изменений.
|
||||
|
||||
|
||||
|
||||
## 2. Брейкпоинты и адаптивность
|
||||
|
||||
### 2.1. Якорные брейкпоинты (база)
|
||||
| Версия | Размер макета (Frame) | Диапазон ресайза |
|
||||
|-------------|----------------|---------------|
|
||||
| Mobile | 375 px | 360 – 479 px |
|
||||
| Mobile Wide | 560 px | 480 – 767 px |
|
||||
| Tablet | 992 px | 768 – 1023 px |
|
||||
| Laptop | 1280 px | 1024 – 1439 px|
|
||||
| Desktop | 1640 px | 1440 – 1920 px|
|
||||
|
||||
Требования:
|
||||
- Каждый брейкпоинт — отдельный **Frame** (Screen Frame).
|
||||
- Диапазоны рассматриваются как **интервалы ресайза**: макет должен работать от min до max.
|
||||
- Все брейкпоинты должны быть **логически и визуально связаны**: типографика, сетка, шаги отступов и компоненты должны «эволюционировать», а не пересобираться хаотично.
|
||||
|
||||
> Примечание про границы: диапазоны заданы без пересечений. Пороговые значения: 480/768/1024/1440. Пример правила: `Mobile ≤ 479`, `Mobile Wide 480–767`, `Tablet 768–1023`, `Laptop 1024–1439`, `Desktop ≥ 1440`.
|
||||
|
||||
### 2.2. Когда допустимо менять список брейкпоинтов
|
||||
Базовый набор можно корректировать **только** при объективной необходимости:
|
||||
- меняется **навигационный паттерн** (например, горизонтальное меню → бургер);
|
||||
- меняется **сетка** (количество колонок/маржины/контейнер);
|
||||
- меняется **иерархия контента** (скрытие/появление блока, перестановка основных зон);
|
||||
- появляются «скачки» из-за контента, которые нельзя закрыть корректным ресайзом.
|
||||
|
||||
Недопустимая причина:
|
||||
- «так удобнее рисовать» или «не получилось настроить авто-лейаут». В этом случае исправляется настройка, а не множатся фреймы.
|
||||
|
||||
### 2.3. Принцип «один фрейм — один диапазон»
|
||||
Один фрейм описывает **поведение интерфейса в диапазоне**, а не конкретное устройство.
|
||||
|
||||
Дополнительный фрейм внутри диапазона создаётся только если:
|
||||
- на определённой ширине меняется **структура** (например, 2 колонки → 3 колонки);
|
||||
- меняется **тип навигации**;
|
||||
- меняется **композиция** ключевых зон (например, фильтры из сайдбара уходят в drawer).
|
||||
|
||||
### 2.4. Обязательное требование: корректный ресайз
|
||||
Макет считается адаптивным только если он корректно ресайзится вручную **во всём диапазоне**.
|
||||
|
||||
Проверка (обязательная):
|
||||
- вручную измените ширину фрейма до **минимума** диапазона;
|
||||
- вручную измените ширину фрейма до **максимума** диапазона;
|
||||
- проверьте промежуточные значения (минимум 3–5 точек внутри диапазона).
|
||||
|
||||
Оценивается:
|
||||
- поведение сетки и контейнеров;
|
||||
- перенос и переполнение текста;
|
||||
- колонки, карточки и списки;
|
||||
- навигация и хедер/футер;
|
||||
- отсутствие коллизий: наездов, обрезаний, неожиданных перекрытий;
|
||||
- сохранение визуальной иерархии.
|
||||
|
||||
> Макет некорректен, если «работает» только на базовой ширине, а при ресайзе ломается.
|
||||
|
||||
### 2.5. Реализация адаптивности в Figma (обязательные правила)
|
||||
|
||||
#### 2.5.1. Auto Layout
|
||||
Обязателен для:
|
||||
- всех контейнеров списков и лент;
|
||||
- карточек, таблиц, форм;
|
||||
- хедера/футера/сайдбара;
|
||||
- любых повторяющихся групп (где есть паддинги и расстояния).
|
||||
|
||||
Минимальные требования к Auto Layout:
|
||||
- паддинги задаются через Auto Layout (а не «ручными» прямоугольниками);
|
||||
- расстояния между элементами задаются через `Spacing`;
|
||||
- направление (Horizontal/Vertical) и `Wrap` выбираются обоснованно;
|
||||
- в ключевых местах задаются **min/max** (если без этого появляются «переломы» компоновки).
|
||||
|
||||
#### 2.5.2. Resizing: Hug / Fill / Fixed
|
||||
Правила выбора:
|
||||
- **Контейнеры** (строки, колонки, секции) чаще всего: `Fill container` по ширине.
|
||||
- **Элементы по контенту** (лейблы, чипсы, иконки): `Hug contents`.
|
||||
- **Фиксированные размеры** — только для:
|
||||
- иконок (например, 16/20/24);
|
||||
- интерактивных областей, где фикс диктуется UX (например, высота кнопки);
|
||||
- медиа с заданным форм-фактором (но с контролем кропа).
|
||||
|
||||
Типичная ошибка: «всё Fixed». Это убивает адаптивность.
|
||||
|
||||
#### 2.5.3. Constraints
|
||||
Constraints применяются там, где Auto Layout не покрывает сценарий (например, декоративные элементы, фоновые иллюстрации, абсолютные позиционирования).
|
||||
|
||||
Минимальные правила:
|
||||
- элемент, который должен «прилипать» к краю контейнера, получает соответствующие привязки (`Left/Right/Top/Bottom`);
|
||||
- элементы, которые должны масштабироваться пропорционально, используют `Scale` только если это оправдано (иначе деформируется типографика и иконки);
|
||||
- избегайте смешения: Auto Layout внутри контейнера + хаотичные constraints у детей.
|
||||
|
||||
### 2.6. Примеры адаптивного поведения (как описывать)
|
||||
В странице `Responsive / Breakpoints` для каждой сложной зоны должны быть описаны правила в формате:
|
||||
- **Что фиксировано** (например, высота хедера 64px)
|
||||
- **Что тянется** (контейнер контента по ширине)
|
||||
- **Что переносится/перестраивается** (карточки wrap, 1→2→3 колонки)
|
||||
- **Что скрывается/появляется** (например, вторичный CTA скрывается на mobile)
|
||||
|
||||
Пример описания (текстом):
|
||||
- «Сетка карточек: на Mobile — 1 колонка, на Tablet — 2 колонки, на Laptop/ Desktop — 3 колонки. Карточка имеет min-width 280px, при нехватке места переносится на новую строку (wrap).»
|
||||
|
||||
|
||||
|
||||
## 3. Компоненты
|
||||
|
||||
### 3.1. Что обязано быть компонентом
|
||||
Компонентами должны быть оформлены:
|
||||
- любые повторяющиеся UI-элементы (кнопки, поля, карточки, бейджи, таблицы, модалки);
|
||||
- сложные блоки, встречающиеся минимум 2 раза;
|
||||
- элементы с состояниями/интерактивностью.
|
||||
|
||||
Запрещено:
|
||||
- копировать элементы «вручную» вместо компонентов;
|
||||
- создавать отдельные компоненты под каждый брейкпоинт, если поведение можно решить ресайзом.
|
||||
|
||||
### 3.2. Variants вместо копий
|
||||
Состояния и вариации должны быть в **Variants**.
|
||||
|
||||
Минимальный набор состояний (если применимо):
|
||||
- `Default`, `Hover`, `Pressed/Active`, `Disabled`, `Focus`.
|
||||
|
||||
Дополнительные состояния по необходимости:
|
||||
- `Loading`, `Error`, `Success`, `Empty`.
|
||||
|
||||
### 3.3. Требования к адаптивности компонентов
|
||||
Компонент считается адаптивным, если:
|
||||
- корректно меняет ширину при `Fill` (когда вставлен в растягиваемый контейнер);
|
||||
- корректно меняет высоту при изменении контента (например, многострочный текст);
|
||||
- не ломает паддинги и расстояния при ресайзе;
|
||||
- корректно работает внутри разных контейнеров (list/grid, modal, sidebar).
|
||||
|
||||
Обязательная проверка:
|
||||
- вставьте компонент в контейнер разной ширины и измените ширину родителя;
|
||||
- проверьте состояния и длину текста.
|
||||
|
||||
### 3.4. Типовые требования по компонентам (примерно, без привязки к бренду)
|
||||
- **Button**: фиксированная высота (например, 40/44/48 по дизайн-системе), ширина `Hug` или `Fill` по контексту, текст не должен вылезать (правила: перенос/обрезка задаются обоснованно).
|
||||
- **Input**: высота фиксирована, поле `Fill`, лейбл и хелпер-текст — `Hug` с переносом.
|
||||
- **Card**: контейнер `Fill`, текстовые блоки — `Hug` по высоте, изображение — обоснованный `Fill/Fit` с safe-area.
|
||||
|
||||
|
||||
|
||||
## 4. Сетка / Layout Grid
|
||||
|
||||
### 4.1. Зачем нужна сетка
|
||||
Сетка задаёт предсказуемую структуру для:
|
||||
- выравнивания и ритма;
|
||||
- повторяемости решений;
|
||||
- соответствия верстке.
|
||||
|
||||
### 4.2. Базовая рекомендация по колонкам
|
||||
| Device | Колонки |
|
||||
|---------|---------|
|
||||
| Mobile | 4 |
|
||||
| Tablet | 8 |
|
||||
| Desktop | 12 |
|
||||
|
||||
Примечания:
|
||||
- `Laptop` обычно использует 12 колонок (как Desktop), но может отличаться маржинами.
|
||||
- Внутренний контейнер (max-width) допускается, если продукт предполагает «центрированный контент».
|
||||
|
||||
### 4.3. Настройки (что должно быть задано)
|
||||
Для каждого фрейма должны быть явно определены:
|
||||
- `margin` (внешние поля);
|
||||
- `gutter` (межколоночные расстояния);
|
||||
- поведение сетки (`Stretch`/`Center`) — в зависимости от выбранной модели.
|
||||
|
||||
Требование:
|
||||
- сетка в Figma должна **соответствовать** принятой сетке в верстке (или быть согласована до начала дизайна).
|
||||
|
||||
### 4.4. Пример логики перестроения с сеткой
|
||||
- На Mobile: контент в 4 колонки, карточки занимают 4/4.
|
||||
- На Tablet: 8 колонок, карточки 4/8 (2 колонки на ряд).
|
||||
- На Desktop: 12 колонок, карточки 4/12 (3 колонки на ряд).
|
||||
|
||||
---
|
||||
|
||||
## 5. Типографика
|
||||
|
||||
### 5.1. Text Styles + Variables (обязательное правило)
|
||||
Все текстовые слои в макете обязаны использовать **Text Styles**.
|
||||
|
||||
Text Styles должны быть **семантическими** (а не «по экрану») и опираться на **типографические токены в Variables**.
|
||||
|
||||
Минимальный набор (пример семантики):
|
||||
- `H1`, `H2`, `H3`
|
||||
- `Body / Regular`, `Body / Medium`
|
||||
- `Caption`
|
||||
- `Button`
|
||||
|
||||
Требования:
|
||||
- одинаковая семантика должна выглядеть согласованно во всех экранах;
|
||||
- локальные «ручные» правки размеров/интервалов поверх стиля недопустимы (кроме редких исключений, зафиксированных правилом);
|
||||
- разрешены только бесплатные шрифты; предпочтительно использовать **Google Fonts**;
|
||||
- значения типографики (минимум `font-size` и `line-height`, при необходимости `letter-spacing`) — источник правды в **Variables**;
|
||||
- адаптация типографики между брейкпоинтами делается через **Modes** в Variables, а не через дублирование текстовых стилей «под каждый брейкпоинт».
|
||||
|
||||
### 5.2. Адаптация типографики между брейкпоинтами через Modes
|
||||
Если типографика меняется между брейкпоинтами, это изменение фиксируется **через Modes** в коллекции Variables.
|
||||
|
||||
Обязательная структура:
|
||||
- есть коллекция Variables для типографики (например: `Typography`);
|
||||
- в коллекции заведены Modes под брейкпоинты (например: `Mobile`, `Mobile Wide`, `Tablet`, `Laptop`, `Desktop` — в точности как принято в проекте);
|
||||
- для каждого семантического стиля (`H1/H2/Body/...`) определены переменные минимум для:
|
||||
- `font-size`;
|
||||
- `line-height`;
|
||||
- опционально: `letter-spacing` (редко и только обоснованно).
|
||||
|
||||
Обязательное правило применения:
|
||||
- Text Styles используют эти переменные как значения типографики;
|
||||
- у каждого фрейма экрана выставлен корректный Mode (Mobile/Mobile Wide/Tablet/Laptop/Desktop), чтобы типографика внутри фрейма соответствовала брейкпоинту;
|
||||
- любые отличия от токенов (если они вообще допускаются) должны быть подписаны: **что изменено**, **зачем** и **как это ведёт себя** на min/max диапазона и при длинном контенте.
|
||||
|
||||
### 5.3. Многострочный текст: переносы и ограничения
|
||||
Для ключевых текстовых блоков обязаны быть определены правила:
|
||||
- переносится ли текст (wrap) или обрезается (truncate);
|
||||
- сколько максимум строк допустимо в конкретном контексте (карточка/ячейка/таблица/заголовок/кнопка);
|
||||
- что происходит при переполнении (например, `…`, перенос на следующую строку, увеличение высоты блока — что именно ожидается в UI).
|
||||
|
||||
Минимальная проверка контента:
|
||||
- короткий текст;
|
||||
- нормальный (ожидаемый);
|
||||
- длинный;
|
||||
- «экстремальный» (очень длинный) — для выявления поломок;
|
||||
- одна очень длинная строка без пробелов (проверка на разъезд/переполнение в узких контейнерах).
|
||||
|
||||
|
||||
|
||||
## 6. Цвета и стили
|
||||
|
||||
### 6.1. Цвета — только через Color Styles
|
||||
Все цвета должны быть оформлены как **Color Styles**.
|
||||
|
||||
Требование по именованию:
|
||||
- семантика важнее оттенка.
|
||||
|
||||
Пример семантических групп:
|
||||
- `Primary / Secondary`
|
||||
- `Text / Background / Border`
|
||||
- `Success / Error / Warning`
|
||||
|
||||
### 6.2. Дополнительные стили
|
||||
Если в проекте используются:
|
||||
- тени,
|
||||
- блюр,
|
||||
- обводки,
|
||||
- радиусы,
|
||||
|
||||
то они должны быть единообразны и по возможности вынесены в стили/токены (в рамках принятых процессов команды).
|
||||
|
||||
|
||||
|
||||
## 7. Контент и состояния UI
|
||||
|
||||
### 7.1. Контентные сценарии
|
||||
Для каждого экрана/секции, где возможны вариации, должны быть проверены:
|
||||
- короткий/длинный текст;
|
||||
- пустые значения;
|
||||
- максимальные значения (например, большие числа);
|
||||
- локализация (если продукт многоязычный) — хотя бы проверка длины.
|
||||
|
||||
### 7.2. Состояния UI (обязательный список)
|
||||
В зависимости от компонента/экрана должны быть представлены состояния:
|
||||
- `Default`
|
||||
- `Hover`
|
||||
- `Active/Pressed`
|
||||
- `Disabled`
|
||||
- `Focus`
|
||||
- `Loading`
|
||||
- `Empty`
|
||||
- `Error`
|
||||
|
||||
Требование:
|
||||
- состояния должны быть реализованы либо в Variants компонентов, либо как отдельные фреймы/слои с явной маркировкой.
|
||||
|
||||
### 7.3. Ошибки и валидация
|
||||
Где есть ввод и формы, необходимо:
|
||||
- указать правила отображения ошибок (текст, цвет, иконка, границы);
|
||||
- учесть переполнение текста ошибки;
|
||||
- показать состояние `Error` и `Focus` одновременно, если применимо.
|
||||
|
||||
|
||||
|
||||
## 8. Изображения и медиа
|
||||
|
||||
### 8.1. Обоснованный выбор Fill / Fit
|
||||
- `Fill` (cover) используется, когда допустима обрезка.
|
||||
- `Fit` (contain) используется, когда обрезка недопустима.
|
||||
|
||||
### 8.2. Safe area для обрезки
|
||||
Если используется `Fill`, у изображений должна быть предусмотрена **безопасная зона** (safe area):
|
||||
- важные объекты (лицо, логотип, текст на фото) не должны попадать под обрезку при изменении пропорций.
|
||||
|
||||
### 8.3. Разные соотношения сторон
|
||||
Требование:
|
||||
- если контент может приходить с разными ratio, дизайн обязан описать поведение:
|
||||
- фиксируем ли высоту блока;
|
||||
- разрешаем ли менять высоту по контенту;
|
||||
- используем ли placeholder/фон.
|
||||
|
||||
|
||||
|
||||
## 9. Навигация
|
||||
|
||||
### 9.1. Паттерны по брейкпоинтам (базово)
|
||||
- Desktop → горизонтальное меню
|
||||
- Tablet → сокращённое меню (или гибрид)
|
||||
- Mobile → бургер / bottom bar
|
||||
|
||||
### 9.2. Требования к адаптивности навигации
|
||||
- при уменьшении ширины не должно происходить «переполнения» пунктов меню;
|
||||
- должны быть определены правила:
|
||||
- что уходит в «ещё»/overflow;
|
||||
- когда включается бургер;
|
||||
- как ведут себя вторичные действия.
|
||||
|
||||
### 9.3. Сложные случаи
|
||||
Если навигация включает:
|
||||
- табы,
|
||||
- фильтры,
|
||||
- хлебные крошки,
|
||||
|
||||
то должно быть описано поведение на узких ширинах (скролл, перенос, сокращение, скрытие).
|
||||
|
||||
|
||||
|
||||
## 10. Accessibility (A11y)
|
||||
|
||||
### 10.1. Контраст
|
||||
Требование:
|
||||
- контраст текста и интерактивных элементов должен быть не ниже **WCAG AA** (если продуктом не задан иной стандарт).
|
||||
|
||||
### 10.2. Размер клика (hit area)
|
||||
Требование:
|
||||
- минимальная интерактивная зона: **44×44 px**.
|
||||
|
||||
### 10.3. Focus states и порядок
|
||||
Обязательно:
|
||||
- состояние `Focus` для интерактивных элементов;
|
||||
- логичный `tab order` (особенно в формах и диалогах).
|
||||
|
||||
|
||||
|
||||
## 11. Прототипирование
|
||||
|
||||
### 11.1. Что обязательно прототипировать
|
||||
- основные пользовательские флоу (критические пути);
|
||||
- ключевые состояния (ошибка/пусто/загрузка) — если важны для сценария;
|
||||
- диалоги, drawer, dropdown — если это влияет на понимание поведения.
|
||||
|
||||
### 11.2. Переходы между брейкпоинтами
|
||||
Если в продукте предполагается использование интерфейса в разных ширинах (веб), необходимо:
|
||||
- зафиксировать, как ведёт себя интерфейс при переходе через пороги (например, при ресайзе окна).
|
||||
|
||||
### 11.3. Проверка поведения при ресайзе
|
||||
В прототипе или на странице `Responsive / Breakpoints` необходимо продемонстрировать:
|
||||
- как перестраиваются блоки;
|
||||
- что скрывается/появляется;
|
||||
- где меняется навигационный паттерн.
|
||||
|
||||
|
||||
|
||||
## 12. QA и проверка адаптивности
|
||||
|
||||
### 12.1. Обязательный сценарий проверки
|
||||
1) Ресайз фрейма вручную до min диапазона.
|
||||
2) Ресайз до max диапазона.
|
||||
3) Проверка промежуточных значений.
|
||||
4) Проверка отдельных компонентов (в изоляции).
|
||||
5) Проверка экстремальных контентных значений.
|
||||
|
||||
### 12.2. Типовые ошибки (что искать)
|
||||
- текст выходит за границы;
|
||||
- кнопки/поля «схлопываются» ниже допустимого;
|
||||
- пропадает сеточный ритм;
|
||||
- карточки ломают высоту соседей непредсказуемо;
|
||||
- элементы перекрывают друг друга при ресайзе;
|
||||
- компоненты не тянутся, потому что внутри `Fixed` вместо `Fill/Hug`.
|
||||
|
||||
|
||||
|
||||
## 13. Передача разработчику
|
||||
|
||||
### 13.1. Dev Mode
|
||||
Требование:
|
||||
- Dev Mode включён и доступен;
|
||||
- структуры слоёв достаточно для инспекта (без лишнего мусора и без превращения текста в вектор).
|
||||
|
||||
### 13.2. Комментарии и пояснения
|
||||
Должны быть комментарии/аннотации по:
|
||||
- брейкпоинтам и порогам изменений;
|
||||
- правилам скрытия/появления блоков;
|
||||
- нестандартным случаям (например, «на 1024 фильтры переезжают в drawer»).
|
||||
|
||||
### 13.3. Экспорт ассетов
|
||||
Если требуется экспорт:
|
||||
- указать форматы и размеры;
|
||||
- убедиться, что иконки/иллюстрации подготовлены корректно (без лишних слоёв, с понятными именами).
|
||||
|
||||
|
||||
|
||||
## 14. Запрещено
|
||||
|
||||
- фиксированные ширины без причины (если элемент обязан тянуться);
|
||||
- дубли компонентов под каждый экран вместо нормального ресайза;
|
||||
- текст как вектор;
|
||||
- «ручные» отступы вместо Auto Layout;
|
||||
- отсутствие Auto Layout там, где есть паддинги/списки/повторяемые структуры.
|
||||
|
||||
|
||||
|
||||
## 15. Минимальный стандарт качества (acceptance criteria)
|
||||
|
||||
Макет считается адаптивным и готовым к разработке, если:
|
||||
1. Корректно ресайзится во всём диапазоне каждого брейкпоинта.
|
||||
2. Использует Auto Layout + корректные Resizing (Hug/Fill/Fixed) и Constraints.
|
||||
3. Имеет якорные брейкпоинты, а дополнительные фреймы появляются только при структурных изменениях.
|
||||
4. Компоненты оформлены как Components с Variants, состояния UI показаны.
|
||||
5. Понятен разработчику без дополнительных «устных» пояснений: правила адаптивности описаны в файле.
|
||||
@@ -0,0 +1,156 @@
|
||||
# Требования к адаптивному дизайну в Figma (Сжатая версия)
|
||||
|
||||
> Примечание: в этом документе «обоснованно» означает: по каждому «выбору» (например: `Hug/Fill/Fixed`, wrap/truncate для текста, `Fill/Fit`) понятно **что выбрано**, **зачем** и **как это ведёт себя** на min/max диапазона и при длинном контенте.
|
||||
|
||||
## 1. Структура файла Figma
|
||||
Рекомендуемая структура:
|
||||
|
||||
- **Cover / Overview** — описание проекта, цели, контакты, ссылки
|
||||
- **Design / UI** — финальные экраны
|
||||
- **Components** — повторяющиеся элементы и Variants
|
||||
- **Styles / Tokens** — цвета, типографика, эффекты
|
||||
- **Responsive / Breakpoints** — логика адаптивного поведения
|
||||
- **Prototype** — интерактивные сценарии и флоу
|
||||
- **Archive / Old** — устаревшие версии
|
||||
|
||||
> Примечание: Страницы `Responsive / Breakpoints` и `Prototype` не дублируют дизайн-экраны, служат для объяснения поведения интерфейса и интерактивности.
|
||||
|
||||
|
||||
|
||||
## 2. Брейкпоинты и адаптивность
|
||||
|
||||
### 2.1. Якорные брейкпоинты
|
||||
| Версия | Размер макета (Frame) | Диапазон (CSS px) |
|
||||
|---------------|----------------|-------------------|
|
||||
| Mobile | 375 px | 360 – 479 px |
|
||||
| Mobile Wide | 560 px | 480 – 767 px |
|
||||
| Tablet | 992 px | 768 – 1023 px |
|
||||
| Laptop | 1280 px | 1024 – 1439 px |
|
||||
| Desktop | 1640 px | 1440 – 1920 px |
|
||||
|
||||
|
||||
- Каждый брейкпоинт — отдельный Frame в Figma.
|
||||
- Количество брейкпоинтов **может корректироваться** (визуальная иерархия, контент, сетка, навигация).
|
||||
- Все брейкпоинты должны быть **логически и визуально связаны**.
|
||||
|
||||
### 2.2. Принцип «один фрейм — один диапазон»
|
||||
- Один фрейм описывает **диапазон ширин**, а не отдельное устройство.
|
||||
- Дополнительный фрейм — только при изменении сетки, навигации или структуры контента.
|
||||
|
||||
### 2.3. Обязательное требование к ресайзу
|
||||
- Макеты должны **корректно ресайзиться во всём диапазоне**, без дополнительных фреймов.
|
||||
- Проверяется:
|
||||
- изменение ширины фрейма вручную до min/max диапазона
|
||||
- поведение сетки, карточек, текста, навигации
|
||||
- отсутствие коллизий и переполнений
|
||||
- сохранение визуальной иерархии
|
||||
|
||||
> Макет некорректен, если работает только на базовой ширине.
|
||||
|
||||
### 2.4. Реализация в Figma
|
||||
- **Обязательно:** Auto Layout на всех контейнерах, обоснованный выбор `Hug / Fill / Fixed`, Constraints, min/max width для ключевых блоков.
|
||||
- **Запрещено:** ручное позиционирование без Auto Layout, абсолютные размеры без необходимости, дубли фреймов для адаптивности.
|
||||
|
||||
|
||||
|
||||
## 3. Компоненты
|
||||
- Все повторяющиеся элементы — **Components**; использовать **Variants** вместо копий.
|
||||
- Адаптивность компонентов:
|
||||
- тянутся по ширине
|
||||
- меняют высоту по контенту
|
||||
- корректно ведут себя в разных контейнерах
|
||||
|
||||
|
||||
|
||||
## 4. Сетка / Layout Grid
|
||||
| Device | Колонки |
|
||||
|---------|---------|
|
||||
| Mobile | 4 |
|
||||
| Tablet | 8 |
|
||||
| Desktop | 12 |
|
||||
|
||||
- Настройки: margin, gutter, column width
|
||||
- Сетка должна соответствовать верстке
|
||||
|
||||
|
||||
|
||||
## 5. Типографика
|
||||
- Только через **Text Styles** (семантика: `H1 / H2 / Body / Caption / Button`)
|
||||
- Только бесплатные шрифты (предпочтительно **Google Fonts**)
|
||||
- Значения типографики (минимум размер и line-height) — через **Variables**
|
||||
- Адаптация между брейкпоинтами — через **Modes** в Variables (Mobile/Mobile Wide/Tablet/Laptop/Desktop)
|
||||
- Для ключевых текстов определены правила: wrap/truncate и max lines
|
||||
|
||||
|
||||
|
||||
## 6. Цвета и стили
|
||||
- **Color Styles** с семантическими названиями:
|
||||
`Primary / Secondary`, `Text / Background / Border`, `Success / Error / Warning`
|
||||
|
||||
|
||||
|
||||
## 7. Контент и состояния UI
|
||||
- Текст: короткий / нормальный / длинный, проверка переполнения
|
||||
- Состояния UI: Default, Hover, Active, Disabled, Focus, Loading, Empty, Error
|
||||
|
||||
|
||||
|
||||
## 8. Изображения и медиа
|
||||
- Использовать `Fill / Fit` обоснованно
|
||||
- Safe area для обрезки
|
||||
- Поддержка разных соотношений сторон
|
||||
- Форматы и размеры соответствуют проекту
|
||||
|
||||
|
||||
|
||||
## 9. Навигация
|
||||
- Desktop → горизонтальное меню
|
||||
- Tablet → сокращённое меню
|
||||
- Mobile → бургер / bottom bar
|
||||
|
||||
|
||||
|
||||
## 10. Accessibility (A11y)
|
||||
- Контраст ≥ WCAG AA
|
||||
- Минимальный размер клика: 44×44 px
|
||||
- Focus states, логичный tab order
|
||||
|
||||
|
||||
|
||||
## 11. Прототипирование
|
||||
- Основные пользовательские флоу
|
||||
- Переходы между брейкпоинтами
|
||||
- Проверка поведения при ресайзе
|
||||
|
||||
|
||||
|
||||
## 12. QA и проверка адаптивности
|
||||
- Ресайз фрейма вручную до min/max диапазона
|
||||
- Проверка компонентов отдельно
|
||||
- Проверка экстремальных значений
|
||||
- Макет готов, если ресайз работает корректно во всём диапазоне и нет коллизий
|
||||
|
||||
|
||||
|
||||
## 13. Передача разработчику
|
||||
- Dev Mode включён
|
||||
- Комментарии с логикой адаптивности
|
||||
- Описаны брейкпоинты, поведение блоков, скрытие/появление элементов
|
||||
|
||||
|
||||
|
||||
## 14. Запрещено
|
||||
- Фиксированные ширины без причины
|
||||
- Дубли компонентов под каждый экран
|
||||
- Текст как вектор
|
||||
- Ручные отступы
|
||||
- Отсутствие Auto Layout
|
||||
|
||||
|
||||
|
||||
## 15. Минимальный стандарт качества
|
||||
Макет считается адаптивным, если:
|
||||
1. Корректно ресайзится во всём диапазоне
|
||||
2. Использует Auto Layout + Constraints
|
||||
3. Имеет брейкпоинты
|
||||
4. Понятен разработчику без дополнительных объяснений
|
||||
30
projects/figma-adaptive-standards/canons/index.md
Normal file
30
projects/figma-adaptive-standards/canons/index.md
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
title: Figma Adaptive Standards
|
||||
description: Карта стандартов подготовки адаптивных макетов в Figma для передачи в разработку
|
||||
|
||||
|
||||
# Figma Adaptive Standards
|
||||
|
||||
Figma Adaptive Standards — набор требований к подготовке адаптивных макетов в Figma. Документация фиксирует, как описывать брейкпоинты, проверять ресайз в диапазоне, настраивать Auto Layout и Constraints, оформлять компоненты, сетки, типографику, состояния интерфейса, A11y и передачу в разработку.
|
||||
|
||||
Стандарты нужны, чтобы макет был не только визуальной картинкой на нескольких ширинах, а рабочей моделью поведения интерфейса. По документам должно быть понятно, как экран живёт между брейкпоинтами, какие элементы тянутся, какие перестраиваются, что происходит с длинным контентом и где дизайнерское решение требует отдельного пояснения.
|
||||
|
||||
## Для кого
|
||||
|
||||
- **Дизайнерам** — как чеклист качества адаптивного макета перед передачей в разработку.
|
||||
- **Разработчикам** — как источник правил для проверки реализуемости макета и уточнения спорных мест.
|
||||
- **Team Lead / DesignOps** — как единый стандарт приёмки дизайн-файлов и согласования процесса.
|
||||
|
||||
## Разделы
|
||||
|
||||
- [Сжатая версия](/adaptive-layout-requirements/short) — короткий набор требований для быстрого применения в проекте.
|
||||
- [Полная версия](/adaptive-layout-requirements/full) — развёрнутое описание правил, терминов, проверок и типовых решений.
|
||||
- [Чеклист приёмки](/adaptive-layout-requirements/checklist) — практический список проверок перед передачей макета в разработку.
|
||||
|
||||
## Как читать
|
||||
|
||||
Начните со сжатой версии, чтобы понять общий стандарт. Затем используйте полную версию как справочник по спорным вопросам: брейкпоинтам, ресайзу, Auto Layout, компонентам, сетке, типографике и состояниям UI. Перед передачей макета в разработку пройдите чеклист приёмки.
|
||||
|
||||
## Главный принцип
|
||||
|
||||
Адаптивный макет считается готовым не тогда, когда нарисованы несколько статичных фреймов, а когда понятна логика поведения интерфейса во всём диапазоне ширин: от минимального значения до максимального, с длинным контентом, разными состояниями и предсказуемой передачей в разработку.
|
||||
@@ -1,5 +1,7 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import taskLists from 'markdown-it-task-lists';
|
||||
import llmstxt from 'vitepress-plugin-llms';
|
||||
import { themeSyncHead } from '../../../_shared/docs/vitepress/themeHead';
|
||||
import { sidebar, site } from '../docs.config';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -9,9 +11,15 @@ export default defineConfig({
|
||||
outDir: site.outDir,
|
||||
srcDir: 'content',
|
||||
cleanUrls: true,
|
||||
head: [...themeSyncHead],
|
||||
vite: {
|
||||
plugins: [llmstxt()],
|
||||
},
|
||||
markdown: {
|
||||
config(md) {
|
||||
md.use(taskLists);
|
||||
},
|
||||
},
|
||||
themeConfig: {
|
||||
sidebar,
|
||||
socialLinks: [],
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from '../../../../_shared/docs/vitepress/theme';
|
||||
33
projects/figma-adaptive-standards/docs/docs.config.ts
Normal file
33
projects/figma-adaptive-standards/docs/docs.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const site = {
|
||||
title: 'Figma Adaptive Standards',
|
||||
description: 'Стандарты подготовки адаптивных макетов в Figma',
|
||||
base: '/figma-adaptive-standards/',
|
||||
outDir: '../../../public/figma-adaptive-standards',
|
||||
};
|
||||
|
||||
/**
|
||||
* Карта монтирования исходных канонов в VitePress-документацию.
|
||||
*
|
||||
* `source` указывает на markdown-файл внутри `canons/`.
|
||||
* `target` задаёт путь, по которому этот файл попадёт в `docs/content/`
|
||||
* и станет страницей итоговой документации.
|
||||
*/
|
||||
export const mounts = [
|
||||
{ target: 'index.md', source: 'canons/index.md' },
|
||||
{ target: 'overview.md', source: 'canons/index.md' },
|
||||
{ target: 'adaptive-layout-requirements/short.md', source: 'canons/adaptive-layout-requirements.short.md' },
|
||||
{ target: 'adaptive-layout-requirements/full.md', source: 'canons/adaptive-layout-requirements.full.md' },
|
||||
{ target: 'adaptive-layout-requirements/checklist.md', source: 'canons/adaptive-layout-requirements.checklist.md' },
|
||||
];
|
||||
|
||||
export const sidebar = [
|
||||
{
|
||||
text: 'Стандарты',
|
||||
items: [
|
||||
{ text: 'Обзор', link: '/overview' },
|
||||
{ text: 'Сжатая версия', link: '/adaptive-layout-requirements/short' },
|
||||
{ text: 'Полная версия', link: '/adaptive-layout-requirements/full' },
|
||||
{ text: 'Чеклист приёмки', link: '/adaptive-layout-requirements/checklist' },
|
||||
],
|
||||
},
|
||||
];
|
||||
5
projects/figma-adaptive-standards/project.config.ts
Normal file
5
projects/figma-adaptive-standards/project.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
slug: 'figma-adaptive-standards',
|
||||
docsDir: 'docs',
|
||||
archive: true,
|
||||
} as const;
|
||||
20
projects/nextjs-style-guide/build.ts
Normal file
20
projects/nextjs-style-guide/build.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { prepareDocs } from '../_shared/lib/prepare-docs';
|
||||
import { run } from '../_shared/lib/run';
|
||||
import { writeZipFromDirectory } from '../_shared/lib/zip';
|
||||
import config from './project.config';
|
||||
|
||||
const projectDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(projectDir, '../..');
|
||||
const docsDir = path.join(projectDir, config.docsDir);
|
||||
|
||||
await prepareDocs(projectDir, config);
|
||||
run('npx', ['vitepress', 'build', docsDir], rootDir);
|
||||
|
||||
if (config.archive) {
|
||||
const zipPath = path.join(rootDir, 'public', config.slug, `${config.slug}.zip`);
|
||||
writeZipFromDirectory(path.join(docsDir, 'content'), zipPath, config.slug);
|
||||
console.log(`Собран ${path.relative(rootDir, zipPath)}`);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: NextJS Style Guide
|
||||
description: Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
|
||||
description: Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript.
|
||||
---
|
||||
|
||||
# NextJS Style Guide
|
||||
|
||||
Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
|
||||
Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript.
|
||||
|
||||
## Использование
|
||||
|
||||
@@ -16,7 +16,7 @@ description: Стандарты разработки фронтенд-прило
|
||||
|
||||
**Для проекта:**
|
||||
|
||||
- [nextjs-style-guide.zip](/nextjs-style-guide.zip) — Набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
|
||||
- [nextjs-style-guide.zip](/nextjs-style-guide/nextjs-style-guide.zip) — Набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
|
||||
|
||||
## Структура документации
|
||||
|
||||
28
projects/nextjs-style-guide/docs/.vitepress/config.ts
Normal file
28
projects/nextjs-style-guide/docs/.vitepress/config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import taskLists from 'markdown-it-task-lists';
|
||||
import llmstxt from 'vitepress-plugin-llms';
|
||||
import { themeSyncHead } from '../../../_shared/docs/vitepress/themeHead';
|
||||
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,
|
||||
ignoreDeadLinks: [/^\/slm-design\//],
|
||||
head: [...themeSyncHead],
|
||||
vite: {
|
||||
plugins: [llmstxt()],
|
||||
},
|
||||
markdown: {
|
||||
config(md) {
|
||||
md.use(taskLists);
|
||||
},
|
||||
},
|
||||
themeConfig: {
|
||||
sidebar,
|
||||
socialLinks: [],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from '../../../../_shared/docs/vitepress/theme';
|
||||
201
projects/nextjs-style-guide/docs/docs.config.ts
Normal file
201
projects/nextjs-style-guide/docs/docs.config.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
export const site = {
|
||||
title: 'NextJS Style Guide',
|
||||
description: 'Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript',
|
||||
base: '/nextjs-style-guide/',
|
||||
outDir: '../../../public/nextjs-style-guide',
|
||||
};
|
||||
|
||||
/**
|
||||
* Карта монтирования исходных канонов в VitePress-документацию.
|
||||
*
|
||||
* SLM-разделы берутся из проекта `slm-design`, чтобы NextJS-гайд не содержал
|
||||
* собственных дублей архитектурного канона.
|
||||
*/
|
||||
export const mounts = [
|
||||
{ target: 'index.md', source: 'canons/index.md' },
|
||||
{ target: 'workflow.md', source: 'canons/workflow.md' },
|
||||
|
||||
{ target: 'slm-design/architecture/index.md', source: '../slm-design/canons/architecture/index.md' },
|
||||
{ target: 'slm-design/architecture/layers.md', source: '../slm-design/canons/architecture/layers.md' },
|
||||
{ target: 'slm-design/architecture/modules.md', source: '../slm-design/canons/architecture/modules.md' },
|
||||
{ target: 'slm-design/architecture/segments.md', source: '../slm-design/canons/architecture/segments.md' },
|
||||
{ target: 'slm-design/architecture/monorepo.md', source: '../slm-design/canons/architecture/monorepo.md' },
|
||||
{ target: 'slm-design/examples/react/factory.md', source: '../slm-design/canons/examples/react/factory.md' },
|
||||
{ target: 'slm-design/examples/react/factory-composition.md', source: '../slm-design/canons/examples/react/factory-composition.md' },
|
||||
{ target: 'slm-design/examples/react/composition-provider.md', source: '../slm-design/canons/examples/react/composition-provider.md' },
|
||||
|
||||
{ target: 'basics/tech-stack.md', source: 'canons/basics/tech-stack.md' },
|
||||
{ target: 'basics/naming.md', source: 'canons/basics/naming.md' },
|
||||
{ target: 'basics/code-style.md', source: 'canons/basics/code-style.md' },
|
||||
{ target: 'basics/documentation.md', source: 'canons/basics/documentation.md' },
|
||||
{ target: 'basics/typing.md', source: 'canons/basics/typing.md' },
|
||||
|
||||
{ target: 'applied/creating-project/from-template.md', source: 'canons/applied/creating-project/from-template.md' },
|
||||
{ target: 'applied/creating-project/manual.md', source: 'canons/applied/creating-project/manual.md' },
|
||||
{ target: 'applied/creating-project/nextjs.md', source: 'canons/applied/creating-project/nextjs.md' },
|
||||
{ target: 'applied/project-structure.md', source: 'canons/applied/project-structure.md' },
|
||||
{ target: 'applied/page-level.md', source: 'canons/applied/page-level.md' },
|
||||
{ target: 'applied/component.md', source: 'canons/applied/component.md' },
|
||||
{ target: 'applied/module.md', source: 'canons/applied/module.md' },
|
||||
{ target: 'applied/rest-client/index.md', source: 'canons/applied/rest-client/index.md' },
|
||||
{ target: 'applied/rest-client/setup/index.md', source: 'canons/applied/rest-client/setup/index.md' },
|
||||
{ target: 'applied/rest-client/setup/auto.md', source: 'canons/applied/rest-client/setup/auto.md' },
|
||||
{ target: 'applied/rest-client/setup/manual.md', source: 'canons/applied/rest-client/setup/manual.md' },
|
||||
{ target: 'applied/rest-client/setup/hooks.md', source: 'canons/applied/rest-client/setup/hooks.md' },
|
||||
{ target: 'applied/rest-client/usage.md', source: 'canons/applied/rest-client/usage.md' },
|
||||
{ target: 'applied/data-fetch/index.md', source: 'canons/applied/data-fetch/index.md' },
|
||||
{ target: 'applied/data-fetch/server-await.md', source: 'canons/applied/data-fetch/server-await.md' },
|
||||
{ target: 'applied/data-fetch/parallel-server-requests.md', source: 'canons/applied/data-fetch/parallel-server-requests.md' },
|
||||
{ target: 'applied/data-fetch/pass-promise-down.md', source: 'canons/applied/data-fetch/pass-promise-down.md' },
|
||||
{ target: 'applied/data-fetch/client-hooks-initial-data.md', source: 'canons/applied/data-fetch/client-hooks-initial-data.md' },
|
||||
{ target: 'applied/data-fetch/client-get-hook.md', source: 'canons/applied/data-fetch/client-get-hook.md' },
|
||||
{ target: 'applied/data-fetch/business-composition.md', source: 'canons/applied/data-fetch/business-composition.md' },
|
||||
{ target: 'applied/styles/styles-setup.md', source: 'canons/applied/styles/styles-setup.md' },
|
||||
{ target: 'applied/styles/styles-usage.md', source: 'canons/applied/styles/styles-usage.md' },
|
||||
{ target: 'applied/svg-sprites/svg-sprites-intro.md', source: 'canons/applied/svg-sprites/svg-sprites-intro.md' },
|
||||
{ target: 'applied/svg-sprites/svg-sprites-setup.md', source: 'canons/applied/svg-sprites/svg-sprites-setup.md' },
|
||||
{ target: 'applied/svg-sprites/svg-sprites-usage.md', source: 'canons/applied/svg-sprites/svg-sprites-usage.md' },
|
||||
{ target: 'applied/images.md', source: 'canons/applied/images.md' },
|
||||
{ target: 'applied/fonts.md', source: 'canons/applied/fonts.md' },
|
||||
{ target: 'applied/aliases.md', source: 'canons/applied/aliases.md' },
|
||||
{ target: 'applied/templates/templates-intro.md', source: 'canons/applied/templates/templates-intro.md' },
|
||||
{ target: 'applied/templates/templates-setup.md', source: 'canons/applied/templates/templates-setup.md' },
|
||||
{ target: 'applied/templates/templates-create.md', source: 'canons/applied/templates/templates-create.md' },
|
||||
{ target: 'applied/templates/templates-usage.md', source: 'canons/applied/templates/templates-usage.md' },
|
||||
{ target: 'applied/biome.md', source: 'canons/applied/biome.md' },
|
||||
{ target: 'applied/postcss.md', source: 'canons/applied/postcss.md' },
|
||||
{ target: 'applied/vscode.md', source: 'canons/applied/vscode.md' },
|
||||
{ target: 'applied/localization.md', source: 'canons/applied/localization.md' },
|
||||
{ target: 'applied/stores.md', source: 'canons/applied/stores.md' },
|
||||
];
|
||||
|
||||
export const routeRewrites = [
|
||||
{ from: '/docs/basics/architecture', to: '/slm-design/architecture' },
|
||||
{ from: '/architecture', to: '/slm-design/architecture' },
|
||||
{ from: '/examples', to: '/slm-design/examples' },
|
||||
];
|
||||
|
||||
export const sidebar = [
|
||||
{
|
||||
text: 'Подсказки',
|
||||
link: '/workflow',
|
||||
},
|
||||
{
|
||||
text: 'Архитектура',
|
||||
items: [
|
||||
{
|
||||
text: 'Спецификация SLM',
|
||||
items: [
|
||||
{ text: 'Обзор', link: '/slm-design/architecture/' },
|
||||
{ text: 'Слои', link: '/slm-design/architecture/layers' },
|
||||
{ text: 'Модули', link: '/slm-design/architecture/modules' },
|
||||
{ text: 'Сегменты', link: '/slm-design/architecture/segments' },
|
||||
{ text: 'Монорепозитории', link: '/slm-design/architecture/monorepo' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Примеры',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Создание фабрики', link: '/slm-design/examples/react/factory' },
|
||||
{ text: 'Композиция фабрик', link: '/slm-design/examples/react/factory-composition' },
|
||||
{ text: 'Композиция через Provider', link: '/slm-design/examples/react/composition-provider' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Базовые правила',
|
||||
items: [
|
||||
{ text: 'Технологии и библиотеки', link: '/basics/tech-stack' },
|
||||
{ text: 'Именование', link: '/basics/naming' },
|
||||
{ text: 'Стиль кода', link: '/basics/code-style' },
|
||||
{ text: 'Документирование', link: '/basics/documentation' },
|
||||
{ text: 'Типизация', link: '/basics/typing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Прикладные разделы',
|
||||
items: [
|
||||
{
|
||||
text: 'Создание проекта',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Из шаблона', link: '/applied/creating-project/from-template' },
|
||||
{ text: 'По гайду вручную', link: '/applied/creating-project/manual' },
|
||||
{ text: 'Чистый Next.js', link: '/applied/creating-project/nextjs' },
|
||||
],
|
||||
},
|
||||
{ text: 'Структура проекта', link: '/applied/project-structure' },
|
||||
{ text: 'Страницы', link: '/applied/page-level' },
|
||||
{ text: 'Компонент', link: '/applied/component' },
|
||||
{ text: 'Модуль', link: '/applied/module' },
|
||||
{
|
||||
text: 'REST-клиент',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Введение', link: '/applied/rest-client/' },
|
||||
{
|
||||
text: 'Настройка',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Обзор', link: '/applied/rest-client/setup/' },
|
||||
{ text: 'Автогенерация из OpenAPI', link: '/applied/rest-client/setup/auto' },
|
||||
{ text: 'Ручное создание', link: '/applied/rest-client/setup/manual' },
|
||||
{ text: 'GET-хуки REST-клиента', link: '/applied/rest-client/setup/hooks' },
|
||||
],
|
||||
},
|
||||
{ text: 'Использование', link: '/applied/rest-client/usage' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Получение данных',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Обзор', link: '/applied/data-fetch/' },
|
||||
{ text: 'Серверный await', link: '/applied/data-fetch/server-await' },
|
||||
{ text: 'Параллельные серверные запросы', link: '/applied/data-fetch/parallel-server-requests' },
|
||||
{ text: 'Передача промиса ниже', link: '/applied/data-fetch/pass-promise-down' },
|
||||
{ text: 'Начальные данные для клиентских хуков', link: '/applied/data-fetch/client-hooks-initial-data' },
|
||||
{ text: 'Клиентский GET-хук', link: '/applied/data-fetch/client-get-hook' },
|
||||
{ text: 'Business-композиция', link: '/applied/data-fetch/business-composition' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Стили',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Настройка', link: '/applied/styles/styles-setup' },
|
||||
{ text: 'Использование', link: '/applied/styles/styles-usage' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'SVG-спрайты',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Введение', link: '/applied/svg-sprites/svg-sprites-intro' },
|
||||
{ text: 'Настройка', link: '/applied/svg-sprites/svg-sprites-setup' },
|
||||
{ text: 'Использование', link: '/applied/svg-sprites/svg-sprites-usage' },
|
||||
],
|
||||
},
|
||||
{ text: 'Изображения', link: '/applied/images' },
|
||||
{ text: 'Шрифты', link: '/applied/fonts' },
|
||||
{ text: 'Алиасы импортов', link: '/applied/aliases' },
|
||||
{
|
||||
text: 'Шаблоны генерации',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Введение', link: '/applied/templates/templates-intro' },
|
||||
{ text: 'Настройка', link: '/applied/templates/templates-setup' },
|
||||
{ text: 'Создание шаблонов', link: '/applied/templates/templates-create' },
|
||||
{ text: 'Использование', link: '/applied/templates/templates-usage' },
|
||||
],
|
||||
},
|
||||
{ text: 'Biome', link: '/applied/biome' },
|
||||
{ text: 'PostCSS', link: '/applied/postcss' },
|
||||
{ text: 'VS Code', link: '/applied/vscode' },
|
||||
{ text: 'Локализация', link: '/applied/localization' },
|
||||
{ text: 'Stores', link: '/applied/stores' },
|
||||
],
|
||||
},
|
||||
];
|
||||
5
projects/nextjs-style-guide/project.config.ts
Normal file
5
projects/nextjs-style-guide/project.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
slug: 'nextjs-style-guide',
|
||||
docsDir: 'docs',
|
||||
archive: true,
|
||||
} as const;
|
||||
0
projects/nextjs-style-guide/scripts/.gitkeep
Normal file
0
projects/nextjs-style-guide/scripts/.gitkeep
Normal file
22
projects/slm-design/build.ts
Normal file
22
projects/slm-design/build.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { prepareDocs } from '../_shared/lib/prepare-docs';
|
||||
import { run } from '../_shared/lib/run';
|
||||
import { writeZipFromDirectory } from '../_shared/lib/zip';
|
||||
import config from './project.config';
|
||||
|
||||
const projectDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(projectDir, '../..');
|
||||
const docsDir = path.join(projectDir, config.docsDir);
|
||||
|
||||
await prepareDocs(projectDir, config);
|
||||
run('npx', ['vitepress', 'build', docsDir], rootDir);
|
||||
|
||||
if (config.archive) {
|
||||
const zipPath = path.join(rootDir, 'public', config.slug, `${config.slug}.zip`);
|
||||
writeZipFromDirectory(path.join(docsDir, 'content'), zipPath, config.slug);
|
||||
console.log(`Собран ${path.relative(rootDir, zipPath)}`);
|
||||
}
|
||||
|
||||
await import('./scripts/build-skill');
|
||||
@@ -4,6 +4,7 @@ description: Назначение архитектуры, ключевые пр
|
||||
---
|
||||
|
||||
# SLM Design
|
||||
|
||||
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
|
||||
|
||||
## Разделы спецификации
|
||||
@@ -13,12 +14,16 @@ Scoped Layered Module Design — модульная архитектура фр
|
||||
- [Слои](/architecture/layers) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
|
||||
- [Модули](/architecture/modules) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
|
||||
- [Сегменты](/architecture/segments) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
|
||||
- [Монорепозитории](/architecture/monorepo) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business.
|
||||
- [Монорепозитории](/architecture/monorepo) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business/compositions.
|
||||
|
||||
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Единый слой композиции
|
||||
|
||||
Страницы, маршруты и крупные продуктовые части интерфейса собираются в `compositions`. Слой не навязывает жёсткую структуру: команда может использовать `pages/layouts/screens/widgets` или другую организацию под свой фреймворк и продукт.
|
||||
|
||||
### Вертикальная организация домена
|
||||
|
||||
Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы.
|
||||
@@ -29,27 +34,27 @@ Cross-domain зависимости в бизнес-слое реализуют
|
||||
|
||||
### Разделение ответственности без перегрузки слоёв
|
||||
|
||||
Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода.
|
||||
Композиция приложения (`compositions/`), сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — разные слои с разной природой. Ни один слой не превращается в свалку разнородного кода.
|
||||
|
||||
### Графовая композиция там, где она нужна
|
||||
|
||||
Внутри `compositions` допускается граф импортов через публичный API. Это позволяет page-level store, provider или business composition использовать одновременно в layout, screen и widget, не перенося продуктовый runtime-state в `infra` или `shared`.
|
||||
|
||||
### Горизонтальная инкапсуляция
|
||||
|
||||
Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга.
|
||||
Вложенные модули (`parts/`) и публичные API позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга.
|
||||
|
||||
### Колокация по умолчанию
|
||||
|
||||
Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями.
|
||||
|
||||
### Явное разделение каркаса и контента
|
||||
|
||||
Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью.
|
||||
|
||||
### Масштабирование через группировку
|
||||
|
||||
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции).
|
||||
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: композиции по страницам и маршрутам, бизнес-домены по субдоменам, UI-компоненты по уровню абстракции.
|
||||
|
||||
### Адаптация к монорепозиториям
|
||||
|
||||
SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы.
|
||||
SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. `compositions` и бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы.
|
||||
|
||||
## Происхождение
|
||||
|
||||
@@ -66,20 +71,20 @@ SLM Design вырос на основе:
|
||||
src/
|
||||
├── app/
|
||||
│
|
||||
├── layouts/
|
||||
│ ├── main/
|
||||
│ └── dashboard/
|
||||
│
|
||||
├── screens/
|
||||
│ ├── home/
|
||||
│ ├── products/
|
||||
│ ├── product-detail/
|
||||
│ └── about/
|
||||
│
|
||||
├── widgets/
|
||||
│ ├── page-heading/
|
||||
│ ├── hero-section/
|
||||
│ └── promo-banner/
|
||||
├── compositions/
|
||||
│ ├── pages/
|
||||
│ │ ├── home/
|
||||
│ │ ├── profile/
|
||||
│ │ └── product-detail/
|
||||
│ ├── layouts/
|
||||
│ │ ├── main/
|
||||
│ │ └── dashboard/
|
||||
│ ├── screens/
|
||||
│ │ ├── home/
|
||||
│ │ └── profile/
|
||||
│ └── widgets/
|
||||
│ ├── page-heading/
|
||||
│ └── promo-banner/
|
||||
│
|
||||
├── business/
|
||||
│ ├── auth/
|
||||
@@ -108,7 +113,10 @@ src/
|
||||
|
||||
## Принципы
|
||||
|
||||
- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле.
|
||||
- **Композиция — отдельный слой.** Страницы, маршруты и крупные продуктовые части интерфейса собираются в `compositions`.
|
||||
- **Структура композиции свободна.** Команда сама выбирает организацию внутри `compositions`; базовая рекомендация — `pages/layouts/screens/widgets`.
|
||||
- **Домен — единое целое.** Всё, что относится к домену, живёт в одном business-модуле.
|
||||
- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости.
|
||||
- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API.
|
||||
- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда.
|
||||
- **Зависимости однонаправлены за пределами compositions.** Глобальная стрелка: `app → compositions → business → infra → ui → shared`.
|
||||
- **Внутри compositions допустим граф.** Composition modules могут импортировать друг друга через public API.
|
||||
- **Архитектура — каркас, не клетка.** Правила фиксируют границы ответственности и public API, а внутреннюю форму композиции определяет команда.
|
||||
@@ -9,7 +9,7 @@ description: Иерархия слоёв от app до shared, правила з
|
||||
|
||||
## Определение
|
||||
|
||||
**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.**
|
||||
**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.**
|
||||
|
||||
## Группы слоёв
|
||||
|
||||
@@ -17,7 +17,7 @@ description: Иерархия слоёв от app до shared, правила з
|
||||
|
||||
| Группа | Слои | Описание |
|
||||
|--------|------|----------|
|
||||
| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы |
|
||||
| Композиция | `app`, `compositions` | Подключают приложение к фреймворку и собирают страницы, маршруты и крупные продуктовые части интерфейса |
|
||||
| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит |
|
||||
| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги |
|
||||
|
||||
@@ -25,20 +25,24 @@ description: Иерархия слоёв от app до shared, правила з
|
||||
|
||||
Любой импорт между модулями — только через публичный API.
|
||||
|
||||
```
|
||||
app → [ layouts | screens ] → widgets → business → infra → ui → shared
|
||||
```text
|
||||
app → compositions → business → infra → ui → shared
|
||||
```
|
||||
|
||||
- `layouts` и `screens` — параллельные слои, не импортируют друг друга
|
||||
- Модули одного слоя в группе «Композиция» изолированы друг от друга
|
||||
- Модули одного слоя `infra` и `ui` могут импортировать друг друга через публичный API
|
||||
- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую
|
||||
- `app` подключает приложение к фреймворку и импортирует готовые модули из нижних слоёв
|
||||
- `compositions` импортирует `business`, `infra`, `ui`, `shared`
|
||||
- `business` импортирует `infra`, `ui`, `shared`
|
||||
- `infra` импортирует `infra`, `ui`, `shared`
|
||||
- `ui` импортирует `ui` и `shared`
|
||||
- `shared` не импортирует другие SLM-слои
|
||||
- `business`, `infra`, `ui`, `shared` не импортируют `compositions`
|
||||
- Внутри `compositions` направление импортов между composition modules не фиксируется, но импорты разрешены только через публичный API
|
||||
- Модули `business` используют runtime-зависимости на другие домены только через фабрику, `import type` — напрямую
|
||||
- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях
|
||||
|
||||
|
||||
## Слой App
|
||||
|
||||
Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen.
|
||||
Точка входа приложения. Отвечает за запуск, роутинг и подключение composition modules к фреймворку.
|
||||
|
||||
В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации.
|
||||
|
||||
@@ -46,90 +50,77 @@ app → [ layouts | screens ] → widgets → business → infra → ui → shar
|
||||
|
||||
- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация
|
||||
- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов
|
||||
- Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует
|
||||
- Провайдеры, guards, layouts, screens и страницы — только подключает готовые из `compositions` или нижних слоёв, не реализует
|
||||
- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы
|
||||
- Никем не импортируется
|
||||
|
||||
## Слой Layouts
|
||||
## Слой Compositions
|
||||
|
||||
Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar).
|
||||
`compositions/` — слой сборки страниц, маршрутов и крупных продуктовых частей интерфейса.
|
||||
|
||||
На этом слое собираются page, layout, screen, widget и другие composition modules. Они связываются между собой и с нижними слоями: `business`, `infra`, `ui`, `shared`.
|
||||
|
||||
SLM не фиксирует жёсткую структуру внутри `compositions`. Команда выбирает организацию под фреймворк, роутинг, CMS и продуктовую задачу.
|
||||
|
||||
Базовая рекомендация:
|
||||
|
||||
```text
|
||||
src/layouts/
|
||||
├── main/
|
||||
├── dashboard/
|
||||
└── auth/
|
||||
src/compositions/
|
||||
├── pages/
|
||||
├── layouts/
|
||||
├── screens/
|
||||
└── widgets/
|
||||
```
|
||||
|
||||
`pages`, `layouts`, `screens` и `widgets` внутри `compositions` не являются отдельными SLM-слоями. Это типы композиционных модулей.
|
||||
|
||||
Composition module может содержать обычные сегменты SLM: `ui/`, `parts/`, `hooks/`, `stores/`, `services/`, `mappers/`, `types/`, `styles/`, `lib/`, `config/`, `providers/`.
|
||||
|
||||
Page-level store, provider, guard или business composition размещаются внутри page composition module, если они нужны всей странице.
|
||||
|
||||
```text
|
||||
compositions/pages/profile/
|
||||
├── profile.page.tsx
|
||||
├── profile-business-composition.ts
|
||||
├── providers/
|
||||
├── hooks/
|
||||
├── stores/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Layout, screen и widget могут получать данные page composition через публичный API соответствующего composition module.
|
||||
|
||||
```ts
|
||||
import { useProfilePageStore } from '@/compositions/pages/profile'
|
||||
```
|
||||
|
||||
Внутри `compositions` направление импортов между composition modules не фиксируется. Допустим граф, но все импорты идут только через public API.
|
||||
|
||||
```ts
|
||||
// Хорошо
|
||||
import { useProfilePageStore } from '@/compositions/pages/profile'
|
||||
|
||||
// Плохо
|
||||
import { useProfilePageStore } from '@/compositions/pages/profile/hooks/use-profile-page-store.hook'
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Содержит только модули
|
||||
- Не содержит бизнес-логику
|
||||
- Контекстно-зависимые блоки принимает через пропсы от `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
|
||||
- `compositions` содержит composition modules страниц, маршрутов и крупных продуктовых частей интерфейса
|
||||
- Структура внутри `compositions` выбирается командой
|
||||
- Базовая рекомендация: `pages/`, `layouts/`, `screens/`, `widgets/`
|
||||
- `pages`, `layouts`, `screens`, `widgets` внутри `compositions` не являются отдельными SLM-слоями
|
||||
- Providers, stores, guards и business composition размещаются внутри того composition module, которому они принадлежат
|
||||
- Внутри `compositions` импорты между composition modules разрешены в любую сторону, но только через public API
|
||||
- Deep imports внутрь composition modules запрещены
|
||||
- `business`, `infra`, `ui` и `shared` не импортируют `compositions`
|
||||
|
||||
## Слой Business
|
||||
|
||||
Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами.
|
||||
|
||||
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `import type`.
|
||||
Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный API фабрики в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `import type`.
|
||||
|
||||
Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки.
|
||||
|
||||
@@ -160,7 +151,7 @@ src/business/
|
||||
|
||||
- Один модуль = один бизнес-домен
|
||||
- Циклические зависимости между доменами запрещены
|
||||
- Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты
|
||||
- Публичный API фабрики — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты
|
||||
- Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую
|
||||
- Доменные типы (`User`, `Product`) живут здесь, не в `shared/`
|
||||
|
||||
@@ -187,6 +178,7 @@ src/infra/
|
||||
|
||||
- Один модуль = один техсервис
|
||||
- Импортирует `infra/`, `ui/`, `shared/`
|
||||
- Не содержит продуктовые composition modules конкретных страниц или маршрутов
|
||||
|
||||
## Слой UI
|
||||
|
||||
@@ -252,3 +244,4 @@ src/shared/
|
||||
### Требования
|
||||
|
||||
- Не имеет runtime-состояния
|
||||
- Не знает о продуктовых composition modules
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Модули
|
||||
description: Структура модуля, типы (UI, бизнес, инфра), публичный API, отличие модуля от компонента
|
||||
description: Структура модуля, типы (композиционный, UI, бизнес, инфра), публичный API, отличие модуля от компонента
|
||||
---
|
||||
|
||||
# Модули
|
||||
@@ -13,7 +13,7 @@ description: Структура модуля, типы (UI, бизнес, инф
|
||||
|
||||
Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно.
|
||||
|
||||
Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность.
|
||||
Модуль не обязан быть UI-блоком. Это может быть page composition, layout composition, screen composition, widget composition, бизнес-домен, инфраструктурный сервис или UI-kit сущность.
|
||||
|
||||
Главная граница модуля — не папка, а ответственность.
|
||||
|
||||
@@ -63,30 +63,62 @@ auth/
|
||||
|
||||
Примеры модулей:
|
||||
|
||||
- `screens/home/` — модуль страницы.
|
||||
- `widgets/page-heading/` — модуль виджета.
|
||||
- `compositions/pages/home/` — модуль page composition.
|
||||
- `compositions/layouts/main/` — модуль layout composition.
|
||||
- `compositions/screens/profile/` — модуль screen composition.
|
||||
- `compositions/widgets/page-heading/` — модуль widget composition.
|
||||
- `business/auth/` — модуль бизнес-домена.
|
||||
- `infra/theme/` — модуль инфраструктурного сервиса.
|
||||
- `ui/button/` — модуль UI-kit сущности.
|
||||
- `screens/home/parts/hero-section/` — вложенный модуль страницы.
|
||||
- `compositions/pages/home/parts/hero-section/` — вложенный модуль page composition.
|
||||
|
||||
Не считаются модулями:
|
||||
|
||||
- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты.
|
||||
- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`.
|
||||
- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента.
|
||||
- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/`, `providers/` — это сегменты.
|
||||
- `compositions/pages/`, `business/commerce/` — это группы, если в них нет `index.ts`.
|
||||
- `compositions/pages/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента.
|
||||
|
||||
## Типы модулей
|
||||
|
||||
Тип модуля определяет обязательный корневой файл и стартовую структуру.
|
||||
|
||||
### Композиционный модуль
|
||||
|
||||
Композиционный модуль — модуль внутри `compositions`, который участвует в сборке страниц, маршрутов и крупных продуктовых частей интерфейса.
|
||||
|
||||
Он может быть page, layout, screen, widget, block, entry-point, CMS-entry, route segment или другим типом композиции, выбранным командой.
|
||||
|
||||
```text
|
||||
compositions/pages/profile/
|
||||
├── profile.page.tsx
|
||||
├── profile-business-composition.ts
|
||||
├── providers/
|
||||
├── hooks/
|
||||
├── stores/
|
||||
├── parts/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Композиционный модуль может импортировать другие composition modules через public API. Это отличие слоя `compositions`: внутри него допускается графовая композиция.
|
||||
|
||||
При этом deep imports запрещены.
|
||||
|
||||
```ts
|
||||
// Хорошо
|
||||
import { useProfilePageStore } from '@/compositions/pages/profile'
|
||||
|
||||
// Плохо
|
||||
import { useProfilePageStore } from '@/compositions/pages/profile/hooks/use-profile-page-store.hook'
|
||||
```
|
||||
|
||||
### UI-модуль
|
||||
|
||||
Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне:
|
||||
|
||||
```text
|
||||
header/
|
||||
├── header.tsx
|
||||
button/
|
||||
├── button.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
@@ -94,7 +126,7 @@ header/
|
||||
|
||||
### Бизнес-модуль
|
||||
|
||||
Бизнес-модуль — модуль, который строится вокруг публичного runtime API.
|
||||
Бизнес-модуль — модуль, который строится вокруг публичного API фабрики.
|
||||
|
||||
Бизнес-модуль обязан иметь фабрику в корне:
|
||||
|
||||
@@ -105,7 +137,7 @@ auth/
|
||||
└── types/
|
||||
```
|
||||
|
||||
Фабрика возвращает публичный runtime API модуля.
|
||||
Фабрика возвращает публичный API модуля для использования в runtime.
|
||||
|
||||
### Инфраструктурный модуль
|
||||
|
||||
@@ -140,6 +172,7 @@ backend-api/
|
||||
├── {module-name}.tsx # корневой файл модуля (опционален)
|
||||
├── ui/ # компоненты модуля
|
||||
├── parts/ # вложенные модули
|
||||
├── providers/ # провайдеры модуля
|
||||
├── hooks/ # хуки
|
||||
├── stores/ # сторы состояния
|
||||
├── services/ # внешние источники данных
|
||||
@@ -184,32 +217,49 @@ export type { CustomerDeps } from './types/customer-deps.type'
|
||||
export type { CustomerFactory } from './types/customer-factory.type'
|
||||
```
|
||||
|
||||
Composition module экспортирует через `index.ts` только безопасный контракт, который нужен другим composition modules или `app`: page/layout/screen/widget, provider, hooks доступа, типы. Внутренние stores, context objects и функции создания состояния не экспортируются без необходимости.
|
||||
|
||||
Если layout, screen или widget импортируют hooks из page composition, не смешивайте в одном public API готовую page composition и hooks для дочерних модулей: это может создать runtime-цикл.
|
||||
|
||||
```ts
|
||||
// compositions/pages/profile/index.ts
|
||||
export { ProfilePageProvider } from './providers/profile-page.provider'
|
||||
export { useProfilePageStore } from './hooks/use-profile-page-store.hook'
|
||||
export { useProfileBusinessComposition } from './hooks/use-profile-business-composition.hook'
|
||||
|
||||
export type { ProfilePageState } from './types/profile-page-state.type'
|
||||
```
|
||||
|
||||
## Фабрика
|
||||
|
||||
Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля.
|
||||
Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный API фабрики.
|
||||
|
||||
Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика.
|
||||
|
||||
Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью.
|
||||
|
||||
Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция».
|
||||
Компоновка фабрик происходит в модуле-потребителе на слое `compositions`.
|
||||
|
||||
### Примеры
|
||||
|
||||
Пример реализации фабрики в React см. в [Создание фабрики](/examples/react/factory).
|
||||
|
||||
Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](/examples/react/factory-composition).
|
||||
Пример композиции фабрик в React composition module см. в [Композиция фабрик](/examples/react/factory-composition).
|
||||
|
||||
Пример композиции фабрик через React Provider см. в [Композиция через Provider](/examples/react/composition-provider).
|
||||
Пример page-level Provider в React см. в [Композиция через Provider](/examples/react/composition-provider).
|
||||
|
||||
Примеры разных структур слоя `compositions` см. в [Структуры compositions](/examples/react/composition-structures).
|
||||
|
||||
## Жизненный цикл
|
||||
|
||||
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
|
||||
|
||||
- Нужен на одной странице → `screens/{name}/parts/`
|
||||
- Появился в 2+ местах → поднимается по природе:
|
||||
- абстрактный UI → `ui/`
|
||||
- блок с данными/логикой → `widgets/`
|
||||
- представление бизнес-домена → `business/{area}/parts/`
|
||||
- Нужен одной странице, route branch или крупной продуктовой части интерфейса → внутри соответствующего composition module.
|
||||
- Нужен нескольким частям одной страницы → внутри page composition или другого общего composition scope.
|
||||
- Нужен нескольким страницам или маршрутам → отдельный composition module внутри `compositions`.
|
||||
- Абстрактный UI без бизнес-логики → `ui/`.
|
||||
- Представление или сценарий бизнес-домена → `business/{domain}/`.
|
||||
- Технический сервис → `infra/`.
|
||||
- Общая чистая утилита → `shared/`.
|
||||
|
||||
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||
@@ -21,9 +21,7 @@ repo/
|
||||
│ ├── web/
|
||||
│ │ └── src/
|
||||
│ │ ├── app/
|
||||
│ │ ├── layouts/
|
||||
│ │ ├── screens/
|
||||
│ │ ├── widgets/
|
||||
│ │ ├── compositions/
|
||||
│ │ ├── business/
|
||||
│ │ ├── infra/
|
||||
│ │ ├── ui/
|
||||
@@ -78,17 +76,17 @@ packages/shared/
|
||||
|
||||
## Что остаётся в приложении
|
||||
|
||||
Слои `app`, `layouts`, `screens`, `widgets` и `business` остаются внутри конкретного приложения.
|
||||
Слои `app`, `compositions` и `business` остаются внутри конкретного приложения.
|
||||
|
||||
```text
|
||||
apps/web/src/app/
|
||||
apps/web/src/layouts/
|
||||
apps/web/src/screens/
|
||||
apps/web/src/widgets/
|
||||
apps/web/src/compositions/
|
||||
apps/web/src/business/
|
||||
```
|
||||
|
||||
`app`, `layouts` и `screens` привязаны к роутингу, каркасу и страницам конкретного приложения. `widgets` не выносятся в пакеты, потому что это слой композиции интерфейса приложения.
|
||||
`app` привязан к фреймворку и entry points приложения.
|
||||
|
||||
`compositions` привязан к страницам, маршрутам и крупным продуктовым частям интерфейса конкретного приложения. Этот слой не выносится в `packages/*`, потому что отражает продуктовую сборку приложения.
|
||||
|
||||
`business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой.
|
||||
|
||||
@@ -193,7 +191,7 @@ packages -/→ apps
|
||||
Внутри приложения продолжает действовать обычное направление зависимостей SLM.
|
||||
|
||||
```text
|
||||
app → [ layouts | screens ] → widgets → business → infra → ui → shared
|
||||
app → compositions → business → infra → ui → shared
|
||||
```
|
||||
|
||||
Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях.
|
||||
@@ -206,11 +204,11 @@ app → [ layouts | screens ] → widgets → business → infra → ui → shar
|
||||
|
||||
```text
|
||||
# Плохо
|
||||
apps/web/src/screens/home/parts/promo-section/
|
||||
apps/web/src/compositions/pages/home/parts/promo-section/
|
||||
packages/ui/promo-section/
|
||||
```
|
||||
|
||||
Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным `parts/`-модулем.
|
||||
Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным composition module.
|
||||
|
||||
## Конфигурационные пакеты
|
||||
|
||||
@@ -227,7 +225,7 @@ packages/ui/promo-section/
|
||||
- В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей.
|
||||
- `packages/shared` является единым пакетом для переиспользуемых утилит и helpers.
|
||||
- Модуль можно размещать в пакете, если он потенциально будет использоваться в двух и более фронтенд-приложениях.
|
||||
- `business`, `app`, `layouts`, `screens`, `widgets` не выносятся в пакеты.
|
||||
- `app`, `compositions` и `business` не выносятся в пакеты.
|
||||
- Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`.
|
||||
- Пакеты не импортируют приложения.
|
||||
- Межпакетные импорты идут только через публичный API.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user