w-funnel/docs/templates-and-builder.md
2025-09-26 02:44:41 +02:00

233 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Шаблоны экранов и конструктор воронки
Этот документ описывает, из каких частей состоит JSON-конфигурация воронки, какие шаблоны экранов доступны в рантайме и как с ними работает конструктор (builder). Используйте его как справочник при ручном редактировании JSON или при настройке воронки через интерфейс администратора.
## Архитектура воронки
Воронка описывается объектом `FunnelDefinition` и состоит из двух частей: метаданных и списка экранов. Навигация осуществляется по идентификаторам экранов, а состояние (выборы пользователя) хранится отдельно в рантайме.
```ts
interface FunnelDefinition {
meta: {
id: string;
version?: string;
title?: string;
description?: string;
firstScreenId?: string; // стартовый экран, по умолчанию первый в списке
};
defaultTexts?: {
nextButton?: string;
continueButton?: string;
};
screens: ScreenDefinition[]; // набор экранов разных шаблонов
}
```
Каждый экран обязан иметь уникальный `id` и поле `template`, которое выбирает шаблон визуализации. Дополнительно поддерживаются:
- `header` — управляет прогресс-баром, заголовком и кнопкой «Назад». По умолчанию шапка показывается, а прогресс вычисляется автоматически в рантайме.
- `bottomActionButton` — универсальное описание основной кнопки («Продолжить», «Далее» и т. п.). Шаблон может переопределить или скрыть её.
- `navigation` — правила переходов между экранами.
### Навигация
Навигация описывается объектом `NavigationDefinition`:
```ts
interface NavigationDefinition {
defaultNextScreenId?: string; // переход по умолчанию
rules?: Array<{
nextScreenId: string; // куда перейти, если условие выполнено
conditions: Array<{
screenId: string; // экран, чьи ответы проверяем
operator?: "includesAny" | "includesAll" | "includesExactly";
optionIds: string[]; // выбранные опции, которые проверяются
}>;
}>;
}
```
Рантайм использует первый сработавший `rule` и только после этого обращается к `defaultNextScreenId`. Для списков с одиночным выбором и скрытой кнопкой переход совершается автоматически при изменении ответа. Для всех прочих шаблонов пользователь должен нажать действие, сконфигурированное для текущего экрана.
## Шаблоны экранов
Ниже приведено краткое описание каждого шаблона и JSON-поле, которое его конфигурирует.
### Информационный экран (`template: "info"`)
Используется для показа статических сообщений, промо-блоков или инструкций. Обязательные поля — `id`, `template`, `title`. Дополнительно поддерживаются:
- `description` — расширенный текст под заголовком.
- `icon` — эмодзи или картинка. `type` принимает значения `emoji` или `image`, `value` — символ или URL, `size``sm | md | lg | xl`.
- `bottomActionButton` — описание кнопки внизу, если нужно отличное от дефолтного текста.
```json
{
"id": "welcome",
"template": "info",
"title": { "text": "Добро пожаловать" },
"description": { "text": "Заполните короткую анкету, чтобы получить персональное предложение." },
"icon": { "type": "emoji", "value": "👋", "size": "lg" },
"navigation": { "defaultNextScreenId": "question-1" }
}
```
Рантайм выводит заголовок по центру, кнопку «Next» (или `defaultTexts.nextButton`) и позволяет вернуться назад, если это разрешено в `header`. Логика описана в `InfoTemplate` и `buildLayoutQuestionProps` — дополнительные параметры (`font`, `color`, `align`) влияют на типографику.【F:src/components/funnel/templates/InfoTemplate.tsx†L1-L99】【F:src/lib/funnel/types.ts†L74-L131】
### Экран с вопросом и вариантами (`template: "list"`)
Базовый интерактивный экран. Поле `list` описывает варианты ответов:
```json
{
"id": "question-1",
"template": "list",
"title": { "text": "Какой формат подходит?" },
"subtitle": { "text": "Можно выбрать несколько", "color": "muted" },
"list": {
"selectionType": "multi", // или "single"
"options": [
{ "id": "opt-online", "label": "Онлайн" },
{ "id": "opt-offline", "label": "Офлайн", "description": "в вашем городе" }
],
"bottomActionButton": { "text": "Сохранить выбор" }
},
"bottomActionButton": { "show": false },
"navigation": {
"defaultNextScreenId": "calendar",
"rules": [
{
"nextScreenId": "coupon",
"conditions": [{
"screenId": "question-1",
"operator": "includesAll",
"optionIds": ["opt-online", "opt-offline"]
}]
}
]
}
}
```
Особенности:
- `selectionType` определяет поведение: `single` строит радиокнопки, `multi` — чекбоксы. Компоненты `RadioAnswersList` и `SelectAnswersList` получают подготовленные данные из `mapListOptionsToButtons`.
- Кнопка действия может описываться либо на уровне `list.bottomActionButton`, либо через общий `bottomActionButton`. В рантайме она скрывается, если `show: false`. Для списков с одиночным выбором и скрытой кнопкой включается автопереход на следующий экран при изменении ответа.【F:src/components/funnel/templates/ListTemplate.tsx†L1-L109】【F:src/components/funnel/FunnelRuntime.tsx†L73-L199】
- Ответы сохраняются в массиве строк (идентификаторы опций) и используются навигацией и аналитикой.
### Экран выбора даты (`template: "date"`)
Предлагает три выпадающих списка (месяц, день, год) и опциональный блок с отформатированной датой.
```json
{
"id": "calendar",
"template": "date",
"title": { "text": "Когда планируете начать?" },
"subtitle": { "text": "Выберите ориентировочную дату", "color": "muted" },
"dateInput": {
"monthLabel": "Месяц",
"dayLabel": "День",
"yearLabel": "Год",
"showSelectedDate": true,
"selectedDateLabel": "Вы выбрали"
},
"infoMessage": { "text": "Мы не будем делиться датой с третьими лицами." },
"navigation": { "defaultNextScreenId": "contact" }
}
```
Особенности:
- Значение сохраняется как массив `[month, day, year]` внутри `answers` рантайма.
- Кнопка «Next» активируется только после заполнения всех полей. Настройка текстов и подсказок — через объект `dateInput` (placeholder, label, формат для превью).
- При `showSelectedDate: true` под кнопкой появляется подтверждающий блок с читабельной датой.【F:src/components/funnel/templates/DateTemplate.tsx†L1-L209】【F:src/lib/funnel/types.ts†L133-L189】
### Экран формы (`template: "form"`)
Подходит для сбора контактных данных. Поле `fields` содержит список текстовых инпутов со своими правилами.
```json
{
"id": "contact",
"template": "form",
"title": { "text": "Оставьте контакты" },
"fields": [
{ "id": "name", "label": "Имя", "required": true, "maxLength": 60 },
{
"id": "email",
"label": "E-mail",
"type": "email",
"validation": {
"pattern": "^\\S+@\\S+\\.\\S+$",
"message": "Введите корректный e-mail"
}
}
],
"validationMessages": {
"required": "Поле ${field} обязательно",
"invalidFormat": "Неверный формат"
},
"navigation": { "defaultNextScreenId": "coupon" }
}
```
Особенности рантайма:
- Локальное состояние синхронизируется с глобальным через `onFormDataChange` — данные сериализуются в JSON-строку и хранятся в массиве ответов (первый элемент).【F:src/components/funnel/FunnelRuntime.tsx†L46-L118】
- Кнопка продолжения (`defaultTexts.continueButton` или «Continue») активна, если все обязательные поля заполнены. Валидаторы проверяют `required`, `maxLength` и регулярное выражение из `validation.pattern` с кастомными сообщениями.【F:src/components/funnel/templates/FormTemplate.tsx†L1-L119】【F:src/lib/funnel/types.ts†L191-L238】
### Экран промокода (`template: "coupon"`)
Отображает купон с акцией и позволяет скопировать промокод.
```json
{
"id": "coupon",
"template": "coupon",
"title": { "text": "Поздравляем!" },
"subtitle": { "text": "Получите скидку" },
"coupon": {
"title": { "text": "Скидка 20%" },
"offer": {
"title": { "text": "-20% на первый заказ" },
"description": { "text": "Действует до конца месяца" }
},
"promoCode": { "text": "START20" },
"footer": { "text": "Скопируйте код и введите при оформлении" }
},
"copiedMessage": "Код {code} скопирован!",
"navigation": { "defaultNextScreenId": "final-info" }
}
```
`CouponTemplate` копирует код в буфер обмена и показывает уведомление `copiedMessage` (строка с подстановкой `{code}`). Кнопка продолжения использует `defaultTexts.continueButton` или значение «Continue».【F:src/components/funnel/templates/CouponTemplate.tsx†L1-L111】【F:src/lib/funnel/types.ts†L191-L230】
## Конструктор (Builder)
Конструктор помогает собирать JSON-конфигурацию и состоит из трёх основных областей:
1. **Верхняя панель** (`BuilderTopBar`). Позволяет создать пустой проект, загрузить готовый JSON и экспортировать текущую конфигурацию. Импорт использует `deserializeFunnelDefinition`, добавляющий служебные координаты для канваса. Экспорт сериализует состояние обратно в формат `FunnelDefinition` (`serializeBuilderState`).【F:src/components/admin/builder/BuilderTopBar.tsx†L1-L79】【F:src/lib/admin/builder/utils.ts†L1-L58】
2. **Канвас** (`BuilderCanvas`). Отображает экраны цепочкой, даёт возможность добавлять новые (`add-screen`), менять порядок drag-and-drop (`reorder-screens`) и выбирать экран для редактирования. Каждый экран показывает тип шаблона, количество опций и ссылку на следующий экран по умолчанию.【F:src/components/admin/builder/BuilderCanvas.tsx†L1-L132】
3. **Боковая панель** (`BuilderSidebar`). Содержит две вкладки состояния:
- Когда экран не выбран, показываются настройки воронки (ID, заголовок, описание, стартовый экран) и сводка валидации (`validateBuilderState`).【F:src/components/admin/builder/BuilderSidebar.tsx†L1-L188】【F:src/lib/admin/builder/validation.ts†L1-L168】
- Для выбранного экрана доступны поля заголовков, параметры списка (тип выбора, опции), правила навигации, кастомизация кнопок и инструмент удаления. Все изменения отправляются через `update-screen`, `update-navigation` и вспомогательные обработчики, формируя корректный JSON.
### Предпросмотр
Компонент `BuilderPreview` визуализирует выбранный экран, используя те же шаблоны, что и боевой рантайм (`ListTemplate`, `InfoTemplate` и др.). Для симуляции действий используются заглушки — выбор опций, заполнение формы и навигация обновляют локальное состояние предпросмотра, но не меняют структуру воронки. При переключении экрана состояние сбрасывается, что позволяет увидеть дефолтное поведение каждого шаблона.【F:src/components/admin/builder/BuilderPreview.tsx†L1-L123】
### Валидация и сериализация
`validateBuilderState` проверяет уникальность идентификаторов экранов и опций, корректность ссылок в навигации и наличие переходов. Ошибки и предупреждения отображаются в боковой панели. При экспорте координаты канваса удаляются, чтобы JSON соответствовал ожиданиям рантайма. Ответы пользователей рантайм хранит в структуре `Record<string, string[]>`, где ключ — `id` экрана, а значение — массив выбранных значений (опций, компонентов даты или сериализованные данные формы).【F:src/lib/admin/builder/validation.ts†L1-L168】【F:src/lib/admin/builder/utils.ts†L1-L86】【F:src/components/funnel/FunnelRuntime.tsx†L1-L215】
## Рабочий процесс
1. Создайте экраны через верхнюю панель или кнопку на канвасе. Каждый новый экран получает уникальный ID (`screen-{n}`).
2. Настройте порядок переходов drag-and-drop и установите `firstScreenId`, если стартовать нужно не с первого элемента.
3. Заполните контент для каждого шаблона, настройте условия в `navigation.rules` и убедитесь, что `defaultNextScreenId` указан для веток без правил.
4. Проверьте сводку валидации — при ошибках экспорт JSON будет возможен, но рантайм может не смочь построить маршрут.
5. Экспортируйте JSON и передайте его рантайму (`<FunnelRuntime funnel={definition} initialScreenId={definition.meta.firstScreenId} />`).
Такой подход гарантирует, что конструктор и рантайм используют одну и ту же схему данных, а визуальные шаблоны ведут себя предсказуемо при изменении конфигурации.