diff --git a/.skaro/tasks/T-023-razdel-oformlenie-v-nastroykah-podklyuch.md b/.skaro/tasks/T-023-razdel-oformlenie-v-nastroykah-podklyuch.md new file mode 100644 index 0000000..d0721d1 --- /dev/null +++ b/.skaro/tasks/T-023-razdel-oformlenie-v-nastroykah-podklyuch.md @@ -0,0 +1,30 @@ +--- +id: T-023 +title: Раздел «Оформление» в настройках подключения +milestone: M04 +status: done +depends_on: + - T-008 + - T-019 + - T-020 +order: 5 +spec: "0021" +created: 2026-09-29 +branch: skaro/T-023-razdel-oformlenie-v-nastroykah +--- + +## Цель + +Администратор настраивает вид виджета в разделе W3 с живым предпросмотром, ровно как в макете (R-2, R-10). + +## Критерии приёмки + +- [x] 8 пресетов цвета и «Свой HEX» с подсказкой контраста: ≥4.5:1 — ок, меньше — предупреждение, неверный HEX — подсказка формата; сохранение не запрещено +- [x] Загрузка иконок кнопки и шапки, переключатели положения, размера и формы +- [x] Поле своего CSS со ссылкой «Классы виджета» и кнопкой «Сбросить» +- [x] Предпросмотр справа меняется сразу; цвета пресетов — данные, а не сырые hex в стилях компонентов +- [x] Все тексты через t() в ru и en, вид совпадает с кадром W3, файлы в пределах NO GOD FILES + +## Итог + +Ветка перенесена на main 52524287, четыре конфликта разрешены с сохранением раздела «Данные с сайта», счетчика полей, типов и обоих словарей. Результат зафиксирован переписанным коммитом 1184c6d7; рабочая папка чистая, main является предком HEAD. После разрешения конфликтов прошли сборка internal-ui, 20 связанных тестов и браузерная проверка всех трех разделов на реальном backend. diff --git a/apps/internal-ui/src/features/integrations/WebIntegrationPage.tsx b/apps/internal-ui/src/features/integrations/WebIntegrationPage.tsx index a635948..afde9ae 100644 --- a/apps/internal-ui/src/features/integrations/WebIntegrationPage.tsx +++ b/apps/internal-ui/src/features/integrations/WebIntegrationPage.tsx @@ -10,6 +10,7 @@ import { WebIntegrationBasics } from "./WebIntegrationBasics"; import { WebIntegrationHeader } from "./WebIntegrationHeader"; import { useWebIntegration } from "./useWebIntegration"; import { SiteFieldsSection } from "./site-fields/SiteFieldsSection"; +import { WebIntegrationAppearance } from "./appearance/WebIntegrationAppearance"; type Section = "basics" | "fields" | "form" | "look" | "danger"; @@ -17,7 +18,7 @@ const sections: SectionMenuItem
[] = [ { key: "basics", label: t("portals.basics"), icon: "settings" }, { key: "fields", label: t("settings.site_data"), icon: "code" }, { key: "form", label: t("settings.pre_chat_form"), icon: "doc", disabled: true }, - { key: "look", label: t("common.appearance"), icon: "paint", disabled: true, divider: true }, + { key: "look", label: t("common.appearance"), icon: "paint", divider: true }, { key: "danger", label: t("settings.delete_connection"), icon: "trash", danger: true }, ]; @@ -55,6 +56,7 @@ export function WebIntegrationPage({ integrationId, onOpenSettings, onBack }: { + {section === "look" && } {confirmingDelete && setConfirmingDelete(false)} onDeleted={onBack} />} diff --git a/apps/internal-ui/src/features/integrations/appearance/AppearancePreview.tsx b/apps/internal-ui/src/features/integrations/appearance/AppearancePreview.tsx new file mode 100644 index 0000000..bee4346 --- /dev/null +++ b/apps/internal-ui/src/features/integrations/appearance/AppearancePreview.tsx @@ -0,0 +1,30 @@ +import { t } from "../../../i18n"; +import { SHAPE_RADIUS, type WidgetAppearance } from "./model"; +import { WidgetIcon } from "./WidgetIcon"; +import "./preview.css"; + +/** Упрощённая копия из W3; демонстрационные реплики принадлежат макету. */ +export function AppearancePreview({ appearance }: { appearance: WidgetAppearance }) { + const side = { [appearance.launcherPosition]: 16 }; + return ; +} diff --git a/apps/internal-ui/src/features/integrations/appearance/ColorSettings.tsx b/apps/internal-ui/src/features/integrations/appearance/ColorSettings.tsx new file mode 100644 index 0000000..058145e --- /dev/null +++ b/apps/internal-ui/src/features/integrations/appearance/ColorSettings.tsx @@ -0,0 +1,41 @@ +import type { CSSProperties } from "react"; + +import { fmt, t } from "../../../i18n"; +import { Icon } from "../../../shared/icons"; +import { COLOR_PRESETS, validHex, whiteContrast } from "./model"; + +export function ColorSettings({ accent, customHex, custom, onPreset, onHex }: { + accent: string; customHex: string; custom: boolean; + onPreset: (accent: string) => void; onHex: (hex: string) => void; +}) { + const invalid = custom && !validHex(customHex); + const ratio = whiteContrast(accent); + const note = invalid ? t("widget_appearance.hex_format") : t(ratio >= 4.5 + ? "widget_appearance.contrast_ok" : "widget_appearance.contrast_warning", { + ratio: fmt.number(ratio, { minimumFractionDigits: 1, maximumFractionDigits: 1 }), + }); + + return ( +
+ {t("widget_appearance.color")} +
+ {COLOR_PRESETS.map(({ hex, label }) => ( + + ))} +
+
+ + {note} +
+
+ ); +} diff --git a/apps/internal-ui/src/features/integrations/appearance/CssSettings.tsx b/apps/internal-ui/src/features/integrations/appearance/CssSettings.tsx new file mode 100644 index 0000000..5615976 --- /dev/null +++ b/apps/internal-ui/src/features/integrations/appearance/CssSettings.tsx @@ -0,0 +1,13 @@ +import { t } from "../../../i18n"; +import { helpArticleUrl } from "../../../shared/help"; + +export function CssSettings({ value, onChange }: { value: string; onChange: (css: string) => void }) { + return
+
+ + {t("widget_appearance.classes")} +
+