diff --git a/.skaro/tasks/T-027-vyvod-html-soglasiya-v-vidzhete.md b/.skaro/tasks/T-027-vyvod-html-soglasiya-v-vidzhete.md
new file mode 100644
index 0000000..e64205c
--- /dev/null
+++ b/.skaro/tasks/T-027-vyvod-html-soglasiya-v-vidzhete.md
@@ -0,0 +1,31 @@
+---
+id: T-027
+title: Вывод HTML согласия в виджете
+milestone: M05
+status: done
+depends_on:
+ - T-026
+order: 3
+spec: "0020"
+created: 2026-10-02
+branch: skaro/T-027-vyvod-html-soglasiya-v-vidzhet
+---
+
+## Цель
+
+Клиент видит в согласии кликабельные ссылки, переносы и выделение — на экране согласия и в форме перед чатом (SPEC-0020 R-16).
+
+## Критерии приёмки
+
+- [x] `ChatBody.tsx` и `PreChatForm.tsx` выводят HTML согласия через один общий компонент, без дублирования.
+- [x] Вид — ровно по кадрам M1, M3, M4: ссылка цвета виджета, полужирная, без подчёркивания, открывается в новой вкладке; жирный — полужирный основного цвета; у списка отступ 18px.
+- [x] «Редакция N»: текст в одну строку — в конце через « · » (M4); с абзацами или списком — отдельной строкой под текстом мелким вторичным цветом (M1, M3).
+- [x] Тест виджета проверяет вывод разметки и то, что текст без тегов показывается как раньше.
+
+## Заметки
+
+Макет: кадры M1, M3, M4 в `design/baseline/Веб-чат · поля и оформление/`; пояснение — `design/baseline/Инструменты агента и данные клиента.md`, п. 2. Виджет доверяет только очищенному сервером тексту из публичной конфигурации; своих правил очистки в виджете не заводить. Цвета — от цвета виджета и токенов, без новых сырых hex.
+
+## Итог
+
+Экран согласия и форма перед чатом выводят очищенный сервером HTML согласия через общий компонент ConsentText со стилями по кадрам M1, M3, M4; подпись редакции стоит в конце строки через « · » либо отдельной строкой под текстом. Добавлен тест виджета; вид проверен в браузере в окружении задачи skaro-chatballs-t-027 на настоящем бэкенде.
diff --git a/apps/web-chat/src/ChatBody.tsx b/apps/web-chat/src/ChatBody.tsx
index 3d6cbd5..488587a 100644
--- a/apps/web-chat/src/ChatBody.tsx
+++ b/apps/web-chat/src/ChatBody.tsx
@@ -2,6 +2,7 @@ import type { RefObject } from "react";
import type { WebConfig, WebMessage } from "./api";
import { BotAvatar } from "./BotIcon";
import { Bubble, SystemMessage, Typing } from "./ChatMessages";
+import { ConsentText } from "./ConsentText";
import { PhoneForm } from "./PhoneForm";
import { t } from "./i18n";
import { widgetClasses as classes } from "./widgetClasses";
@@ -47,6 +48,6 @@ function Unavailable() {
}
function Consent({ config, accent, title }: { config: WebConfig; accent: string; title: string }) {
- return <>
`);
+ expect(form(PLAIN)).toContain(`${PLAIN} · редакция v3`);
+ for (const html of [screen(PLAIN), form(PLAIN)]) expect(html).not.toContain(classes.consentRevision);
+ });
+
+ it("renders sanitized markup on the consent screen and in the pre-chat form", () => {
+ for (const html of [screen(RICH), form(RICH)]) {
+ expect(html).toContain(LINK);
+ expect(html).toContain(" Что мы храним:
имя и контакты;
");
+ expect(html).not.toContain("<");
+ }
+ });
+
+ it("keeps the revision inline for one line and moves it below paragraphs, breaks and lists", () => {
+ const inline = `Вы принимаете ${LINK} и запись чата`;
+ expect(screen(inline)).toContain(" · ред. v3");
+ expect(screen(RICH)).toContain(`