---
id: 05-html-widget-rules
updated: 2026-08-10 17:55 GMT+3
---

# Правила ИИ-HTML (`:::ai_widget`)

Имя блока — **`ai_widget`** (подчёркивание). Не пиши `ai-widget`, `aiwidget` или «просто ссылка на HTML».

ИИ-HTML — самодостаточный HTML. Он **обязан** стоять в материале как блок `:::ai_widget` … `:::/ai_widget`. Без этого блока в редакторе виджета для пользователя нет. Сверстать, залить на CDN и прислать URL — **не** выполнение задачи.

ИИ-HTML всегда часть материала. Он берёт CSS-переменные темы (`--longread-*`) у родительского лонгрида и живёт в его вёрстке. Без лонгрида нет переменных темы, блочных отступов и рендер-контекста. Не делай из ИИ-HTML отдельную страницу.

## Перед create / bake

Не изобретай ИИ-виджет «от себя». Сначала кратко: зачем виджет, что делает пользователь, чем не закрывается штатными блоками (`:::test`, `:::tabs`, …). Дождись согласия («да» / BRIEF ок). Потом локальный `.html` и цикл ниже. Bake/insert — только после отдельного прямого «да».

## Обязательный финал (только IDE-агент с MCP)

Это правило — для Cursor / Claude Code / VS Code с MCP. Чат-боты без MCP (Gemini, ChatGPT и т.п.) bake не делают. Они отдают HTML/markdown кодом. См. сценарий 2 в `README.md`.

С MCP работа в материале готова после вставки `:::ai_widget`. Bake на CDN — **не** на каждой итерации.

### Цикл отладки (канон)

```
локальный .html (+ маркер) → открыть в браузере / править на диске → …
→ спросить «запечь на CDN и вставить в материал?» → ждать прямого «да»
→ create-widget (если ещё нет) → save-widget-draft → bake-widget
→ insert-ai-widget-block (fence = id) → poll archives/ до маркера
```

Пока пользователь не сказал «да» на bake — правь только локальный файл. Не вызывай `bake-widget` «на всякий случай». Не пеки после каждого фикса.

После правок уже вставленного виджета снова правь локальный `.html`. Отладь локально. Снова спроси перед следующим `save-widget-draft` + `bake-widget`. Не bake каждый раз сам.

После bake/insert: poll **`archives/`** (не draft). Затем `get-material`. В `content` должен быть `:::ai_widget` с **id**.

### Локальная копия на ПК (обязательно при MCP)

Интерактив через MCP в Демонстраторе — ок. На диске рабочей машины всё равно нужна HTML-копия. По ней смотришь и правишь без share/CDN-ссылок.

**Обязательно:**

1. Сохрани полный HTML в workspace (`widgets/` или `interactives/`), например `widgets/quiz-onboarding-<widget_id>.html` (или slug без id, пока виджета ещё нет).
2. Отладь локально (открой файл в браузере). Это основной цикл.
3. В ответе укажи **локальный путь**.
4. Bake/insert — только после прямого вопроса и прямого «да» пользователя.

**Запрещено:** bake на каждой правке; только MCP/CDN без файла на диске; «открой по ссылке» как единственный способ смотреть интерактив.

### Запрещено считать задачу сделанной (IDE + MCP)

- Отдать пользователю только CDN/URL HTML («вот ссылка, вставь сам»).
- Остановить работу на `bake-widget` без `insert-ai-widget-block` (когда пользователь уже разрешил bake).
- Поставить в fence **`draft_url`**. Сдать по свежему `get-widget`, пока `archives/` без маркера или мигает.
- Оставить в медиатеке виджет/медиа без читаемого имени («Без названия», пусто, uuid).
- Вписать голый URL в абзац / `![…](url)` / `:::embed` вместо `:::ai_widget`.
- Написать опечатку в типе: `:::ai-widget`, `:::aiwidget`.
- Не оставить локальный `.html` на ПК.
- Запечь на CDN без прямого подтверждения пользователя.

### Чат-бот без MCP

Отдай готовый HTML и/или фрагмент markdown с `:::ai_widget` … `:::/ai_widget`. Bake, CDN и вопрос «запечь?» к тебе не относятся. Пользователь вставит сам.

## Вёрстка и отступы (не липнуть к потолку)

ИИ-HTML живёт **внутри** материала (iframe / карточка блока). Не делай из него полноэкранный лендинг вплотную к краям.

**Обязательно:**

- У корневого контейнера (`body` → первый wrapper) внешний отступ **со всех сторон** ≥ `16px` (лучше `20–24px`). Не ставь `margin: 0; padding: 0` на `html, body` без компенсации.
- Первый визуальный блок (картинка, заголовок, карточка) не должен липнуть к верхнему краю виджета.
- Карточка с картинкой сверху: padding у внешней обёртки или `margin` у карточки от краёв iframe. Full-bleed картинка **внутри** карточки ок, если сама карточка отстоит от края виджета.

**Запрещено:**

```css
html, body { margin: 0; padding: 0; } /* и сразу hero/img на всю ширину без обёртки с отступом */
.card { margin: 0; } /* карточка вплотную к верху iframe */
.hero { width: 100%; margin: 0; } /* картинка к потолку виджета */
```

**Нужно (идея):**

```css
body {
  margin: 0;
  padding: 20px; /* воздух от краёв блока в лонгриде */
  box-sizing: border-box;
}
.card {
  overflow: hidden;
  border-radius: 12px;
  /* картинка может быть edge-to-edge ВНУТРИ card — снаружи уже есть padding body */
}
```

Виджет без верхнего воздуха — брак вёрстки, переделай до bake / insert.

Русский текст внутри виджета — по `06-russian-typography.md` (—, «ёлочки», …).

## Скролл внутри виджета

По умолчанию **без скролла**. Короткие реплики, обратная связь и описание ситуации должны целиком помещаться в кадр. Не ставь `overflow: auto|scroll` и фиксированную высоту «наугад», если контент можно уложить или разбить на шаги.

Скролл — только явный замысел. Пример: длинный текст, который пользователь сознательно читает или открывает дополнительно.

| Контент | Скролл |
|---|---|
| Реплика персонажа | Нет |
| Обратная связь к ответу | Нет |
| Короткое описание ситуации | Нет |
| Развёрнутое объяснение последствий | Да (если иначе не влезает) |
| Уточнение деталей по кнопке | Да |

**Запрещено:** весь экран кейса / квиза в одном скролл-боксе «на всякий случай». Скролл ради ленивой вёрстки короткого текста.

## Тема и цвета (обязательно)

Внутри `:::ai_widget` цвета темы приходят как `--longread-*` на `:root` (сервер / THEME). **Любой цвет в CSS виджета — только `var(--longread-…)`.**

| Переменная | Назначение |
|---|---|
| `--longread-bg` | Фон |
| `--longread-text` | Текст |
| `--longread-surface` | Фон карточек |
| `--longread-surface-text` | Текст на карточках |
| `--longread-headings` | Заголовки (если есть) |
| `--longread-accent` | Акцент |
| `--longread-accent-pale` | Бледный акцент |
| `--longread-positive` | Зелёный |
| `--longread-negative` | Красный |
| `--longread-important-bg` | Фон important |
| `--longread-border` | Границы |

```css
button {
  background: var(--longread-accent);
  color: var(--longread-surface);
}
```

### Запрещено

- Голые `#hex`, `rgb()` / `rgba()` / `hsl()`, `white` / `red` и т.п. в свойствах цвета.
- Свои палитры и `--brand-*` вместо `--longread-*`.
- «Красивый» цвет мимо темы материала.

Self-lint перед сдачей HTML: в `<style>` нет цветовых литералов вне `var(--longread-*)` (кроме согласованного исключения).

Готовых CSS-китов нет — классы свои, **цвета только из переменных**.

`<style id="widget-escape">` — CSS внутри переживает переинжекцию темы; цвета там **всё равно** через `--longread-*`, не hex.

## Запрещено сервером

- `<script src="...">` — внешние скрипты удаляются при bake.
- URL изображений без распознаваемого расширения: `.webp`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`.

## Разрешено

- Инлайн `<script>` без `src`. Синтаксис JavaScript должен быть валидным.
- Инлайн `<style>`.

## Ограничение размера

500 КБ. Сервер отклоняет HTML больше этого лимита.

## Изображения

Только с CDN проекта. URL должен заканчиваться на одно из: `.webp`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`.

## MCP-путь (IDE)

1. Сохрани и отладь HTML локально (`widgets/<slug>.html`). Это основной цикл. Перед bake поставь в HTML **уникальный маркер** (комментарий / `data-build`).
2. Спроси: «запечь на CDN и вставить в материал?» Жди прямого «да». **Не bake без ответа.**
3. `create-widget` — получи `id` черновика (`material_id` / `project_id`). Всегда передай **`name`** — читаемое имя в медиатеке (не uuid, не пусто).
4. `save-widget-draft` — HTML с диска + theme + assets.
5. `bake-widget` — дождись статуса `ready` (ключ `archives/{id}/…`).
6. **`insert-ai-widget-block`** — в fence только **CDN `entry_url`** (после `ready`). **Не** bare id и **не** `draft_url`.
7. Poll archive: `get-resource-status` / `archive-info` → `entry_url` / `archives/…`. Маркер в archive HTML должен совпасть с маркером локального файла. Пока не совпало — poll. Не сдавай по `get-widget` / draft.
8. `get-material` — проверь fence. Пользователю укажи путь к локальному `.html`.

### Шаблон линейного диалога

Для диалогового плеера предпочитай `assemble-linear-dialog` вместо свободного HTML:

1. Сценарий JSON на диске (`widgets/<slug>-scenario.json`) — эталон `docs/templates/linear-dialog/scenario.example.json`. `characters[]` = пул портретов; на реплике — `leftPortraitId`, `rightPortraitId`, `speakerSide`.
2. `create-widget` → `assemble-linear-dialog` (`widget_id` + `scenario`). `save-widget-draft` не нужен.
3. Bake/insert — только после прямого «да». Insert = CDN URL.
4. Фича `ai_widget_templates` — по умолчанию тестеры; при 403 не обходи без согласования.

**Правка уже собранного диалога** (тот же tool):

1. `get-widget` → `scenario` (+ локальный `*-scenario.json`).
2. Правишь JSON (реплики / портреты / пары слотов / локации) — полный объект, не патч HTML.
3. Снова `assemble-linear-dialog` с тем же `widget_id`.
4. Не вызывай `save-widget-draft` и не правь shell плеера руками.
5. Повторный bake — только после «да»; в материал снова CDN `entry_url`.

Имя в медиатеке обязательно. Если создал без имени — сразу `rename-resource`. «Без названия» / пустой ярлык — брак сдачи.

| Слой | В материал? |
|------|-------------|
| Draft / `draft_url` / bare id / свежий `get-widget` | **нет** |
| `archives/{id}/` CDN `entry_url` при `ready` | **да** |

После bake CDN может кратко отдавать старое поколение на том же URL. Держи poll archive. **Не** ставь draft в материал. Повторный bake — только после явного «да».

Повторные правки: снова локально → снова вопрос перед bake. Не пеки на каждой итерации.

Не подменяй insert ссылкой на CDN или draft. Не пропускай локальный файл.
