feat(communications): complete mobile and tablet responsive correction pass with visual QA

This commit is contained in:
ochenstarik 2026-09-11 19:28:46 +07:00
parent 49f5a3447b
commit b1a22b6bf1
53 changed files with 1476 additions and 1084 deletions

1
.gitignore vendored
View file

@ -29,3 +29,4 @@ playwright-report/
.playwright/ .playwright/
e2e/screenshots/ e2e/screenshots/
*.tsbuildinfo *.tsbuildinfo
__pycache__/

View file

@ -1,92 +1,74 @@
# Отчёт о реализации: Перевод Krona Front на дизайн-систему v3.0 и модуль «Коммуникации» (Correction Pass) # Отчёт о реализации: Исправление мобильного и планшетного интерфейса «Коммуникаций»
## 1. Общие сведения ## 1. Общие сведения
- **Проект:** krona-front - **Проект:** krona-front (`https://git.ochenstarik.ru/ochenstarik/krona-front`)
- **Рабочая ветка:** codex/design-system-communications-correction - **Рабочая ветка:** `codex/design-system-communications-mobile-tablet-correction`
- **UI-библиотека:** krona-library (@krona/tokens, @krona/themes, @krona/ui) - **Базовый коммит:** `49f5a34`
- **Стандарт дизайна:** KRONA_DESIGN_SYSTEM_V3.md (версия 3.0: «Благородный зелёный», «Светлый пергамент», «Графитовый тёмный лес») - **UI-библиотека:** krona-library (`@krona/tokens`, `@krona/themes`, `@krona/ui`)
- **Стандарт дизайна:** `KRONA_DESIGN_SYSTEM_V3.md`
- **Эталонные макеты:** - **Эталонные макеты:**
- Коммуникация.png (Desktop 1440/1280) - `Коммуникация.png` (Desktop 1440/1280)
- Коммуникация планшет горизонт.png (Tablet landscape 1024) - `Коммуникация планшет горизонт.png` (Tablet landscape 1024)
- Коммуникация планшет вертикальный.png (Tablet vertical 768) - `Коммуникация планшет вертикальный.png` (Tablet vertical 768)
- Коммуникация телефон.png (Mobile 360/390/430) - `Коммуникация телефон.png` (Mobile 360/390/430)
- **Целевой модуль:** /communications («Коммуникации») - **Целевой модуль:** `/communications` («Коммуникации»)
--- ---
## 2. Архитектура интеграции с дизайн-системой v3.0 ## 2. Ключевые архитектурные решения
### 2.1. Единый источник истины и семантические токены ### 2.1. Адаптивность от реальной ширины рабочей области (Container Queries / ResizeObserver)
Все дизайн-токены, темы оформления и компоненты подключаются напрямую из публичных скомпилированных пакетов библиотеки krona-library: Вместо фиксированных медиа-запросов экрана разметка `MessengersView.tsx` управляется через `ResizeObserver` по фактической ширине контейнера:
- @krona/tokens и @krona/tokens.css: палитра бренда, типографические шкалы (Manrope + Cormorant Garamond), радиусы (--krona-radius-*), тени (--krona-shadow-*), z-индексы и отступы. 1. **Compact (< 600px):** Строгий 1-колоночный drilldown:
- @krona/themes и @krona/themes.css: системные темы оформления v3: - **Уровень 1 (Список):** Полноэкранный список диалогов с поиском и фильтрами.
1. parchment-light — светлая пергаментная тема (теплый фон #F3EAD7 / #F9F6F0, контрастные карточки, хвойный текст #162820); - **Уровень 2 (Чат):** Окно диалога с кнопкой возврата назад и тизером контакта снизу.
2. - **Уровень 3 (Контакт):** Полноэкранная карточка контакта со сделками CRM, задачами и AI-сводкой.
oble-green — фирменная лесная тема (глубокий хвойный #1E3A2F, мягкий фон, акцент шалфея); 2. **Medium (600–949px):** 2 колонки:
3. graphite-forest-dark — ночная графитовая тема (фон #121714, поверхность #1A211D, изумрудный акцент #34D399). - Список диалогов (~280–300px) слева.
- @krona/ui (dist/index.mjs): атомарные компоненты библиотеки (Button, Input, Badge, Avatar, Tabs, Card, ConnectorPill, MessageBubble, Modal). Внешние зависимости React/React-DOM корректно экстернализованы, устраняя конфликт дублирования инстансов React. - Активный диалог справа (занимает все оставшееся пространство, min 320px).
- Сплющенная горизонтальная карточка контакта под чатом **полностью устранена**. Карточка контакта открывается через слайд-овер `Drawer` по кнопке профиля в хедере чата.
3. **Wide (≥ 950px):** 3 колонки:
- Список диалогов (310–330px).
- Активный диалог (min 360px).
- Вертикальная карточка контакта (320–350px).
### 2.2. Управление темами и обратная совместимость ### 2.2. Семантические токены и ликвидация сырых HEX-кодов
Хранилище тем src/stores/theme.ts синхронно устанавливает: - Использованы переменные дизайн-системы V3: `var(--krona-primary)`, `var(--krona-surface)`, `var(--krona-primary-soft)`, `var(--krona-border)`, `var(--krona-gold)`.
- Корневой атрибут data-krona-theme (parchment-light, - Никаких захардкоженных HEX-кодов в прикладной верстке.
oble-green, graphite-forest-dark). - В `src/index.css` обеспечен импорт `@import '@krona/ui.css';`.
- Легаси-атрибут data-theme (light, soft-light, dark) для обратной совместимости с существующими компонентами и тестами.
- Сохранение активной темы в localStorage и поддержку prefers-reduced-motion. ### 2.3. Векторные SVG-портреты
- В `public/avatars/` сгенерированы 11 легковесных SVG-портретов (`ivan.svg`, `olga.svg`, `alexander.svg`, `elena.svg`, `dmitry.svg`, `maria.svg`, `sergey.svg`, `anna.svg`, `maxim.svg`, `irina.svg`, `artem.svg`).
- В моковых данных `mockData.ts` все контакты привязаны к соответствующим локальным SVG-аватарам.
- Компонент `<Avatar src={...} alt={...} initials={...} />` корректно отображает векторные портреты.
### 2.4. Предотвращение скролла всего окна при новых сообщениях
- Заменен вызов `scrollIntoView()` на прямое управление контейнером сообщений:
`messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;`
- Это исключило нежелательный скролл всего документа (`window.scrollY === 0`).
### 2.5. Устранение наложения мобильной нижней навигации
- В `AppShell.tsx` для маршрута `/communications` на мобильных устройствах добавлен отступ `pb-16`.
- Инпут чата и тизер контакта больше не перекрываются фиксированным баром `BottomNav` и доступны для взаимодействия.
--- ---
## 3. Точная реализация макетов модуля «Коммуникации» ## 3. Матрица верификации (Fail-Closed Verification)
### 3.1. Десктоп (>= 1280px) — по макету Коммуникация.png | Проверка | Команда | Результат | Статус |
- **Шапка раздела:** Слоган курсивом *«Больше, чем общение с поддержкой»*, ботаническая гравюра-водяной знак, вкладки *Мессенджеры*, *Email*, *Каналы*, *Контакты*, *Настройки* с бейджами непрочитанных. |---|---|---|:---:|
- **Панель коннекторов:** Пилюли каналов (Все 48, Telegram 12, WhatsApp 8, VK 6, MAX 4, Email 10) и кнопка «Подключить». | **Линтер** | `npm run lint` (`oxlint --deny-warnings`) | 0 warnings, 0 errors (238 файлов) | **PASS** |
- **3 приподнятые скругленные карточки ( | **Компиляция TypeScript** | `npx tsc -b` | Exit code 0 | **PASS** |
ounded-2xl):** | **Модульные и интеграционные тесты** | `npm test` (`vitest run`) | 490/490 тестов в 27 файлах прошли | **PASS** |
1. **Карточка 1 (Список чатов, 320–350px):** Поиск, фильтры («Все», «Непрочитанные», «Избранные»), сортировка, список диалогов со статусом в сети, бейджем канала и непрочитанными. | **E2E тесты (Playwright)** | `npx playwright test e2e/13-communications-visual-and-responsive.spec.ts` | 32/32 тестов во всех 8 вьюпортах прошли | **PASS** |
2. **Карточка 2 (Активный диалог, центр):** | **Продакшн сборка** | `npm run build` (`vite build`) | Собрано за 4.39s без предупреждений | **PASS** |
- Хедер: аватар, имя, статус «в сети», компания, должность, кнопки «В избранное», звонок, видеозвонок, меню.
- Сообщения: входящие белые пузыри с аватаром слева; исходящие шалфейно-оливковые пузыри (#E8EDE4 / #2F3A32) справа с двойной галочкой; карточка вложения PDF («Коммерческое предложение.pdf») с кнопкой «Скачать».
- Панель быстрых ответов (чипсы «Да, подготовим», «Отправить договор», «Нужны реквизиты?», «Назначить встречу» и др.).
- Инпут сообщений: кнопка скрепки, поле ввода с поддержкой Enter, AI-помощник (Sparkles), эмодзи, микрофон, коньячно-бронзовая кнопка отправки (#9A743D).
3. **Карточка 3 (Карточка контакта / CRM, справа):**
- Аватар, имя, должность, компания, бейдж «Клиент», золотой бейдж «VIP», статус «В сети».
- Подключенные каналы (Telegram, WhatsApp, Email) с кнопкой «+ Канал».
- Вкладки: «Инфо», «✦ AI-сводка», «Связи (5)».
- Поля: телефон, email, ответственный менеджер, часовой пояс, заметки.
- Быстрые CRM-действия: **«Создать сделку»** и **«Создать задачу»** с тостом и фиксацией в хранилище.
### 3.2. Планшет (768px – 1279px) — по макетам Коммуникация планшет вертикальный.png и Коммуникация планшет горизонт.png
- 2-колоночный адаптивный макет:
- Левая колонка: список диалогов (300–320px).
- Правая колонка: вертикальный стек, в котором сверху расположен активный чат с полноценным композером сообщений, а снизу — **горизонтальная карточка контакта** с аватаркой, именем, бейджем VIP, списком каналов и кнопками «Создать сделку» / «Создать задачу».
### 3.3. Мобильные устройства (< 768px) — по макету Коммуникация телефон.png
- 1-колоночный 3-уровневый drilldown:
- **Уровень 1 (Список):** Полноэкранный список чатов с коннекторами.
- **Уровень 2 (Диалог):** При выборе чата открывается диалог с кнопкой возврата «Назад», входящими/исходящими сообщениями, чипсами быстрых ответов, композером и фиксированной внизу тизер-плашкой профиля («Иван Петров • Директор • Профиль →»).
- **Уровень 3 (Профиль):** Нажатие на «Профиль» открывает полную карточку контакта с вкладками, деталями и кнопками создания сделки/задачи, с кнопкой возврата «Назад к диалогу».
- Мобильная нижняя навигация не перекрывает элементы управления благодаря расчету отступов (pb-16), минимальный размер кликабельных зон >= 44x44px.
--- ---
## 4. Результаты верификации качества ## 4. Артефакты
| Проверка | Инструмент | Результат | Комментарий | - **Скриншоты тем:** 24 файла (3 темы × 8 вьюпортов) в `artifacts/visual-qa/`
|---|---|---|---| - **Скриншоты состояний:** 7 файлов (`mobile-list.png`, `mobile-chat.png`, `mobile-contact.png`, `tablet-chat.png`, `tablet-contact-drawer.png`, `desktop-without-krona-ai.png`, `desktop-with-krona-ai.png`)
| **Линтер** | oxlint --deny-warnings | **0 warnings, 0 errors** (238 файлов) | Строгий режим без подавлений | - **Манифест скриншотов:** `artifacts/visual-qa/manifest.json`
| **Типизация** | sc -b | **0 errors** (exit code 0) | Строгая проверка TypeScript проектных ссылок | - **Отчёт Visual QA:** `VISUAL_QA.md` (Статус: `READY FOR INDEPENDENT REVIEW`)
| **Unit-тесты** | itest run | **27 test files, 490 passed** (exit code 0) | Включая 11/11 тестов модуля Communications и дизайн-системы V3 |
| **Production Build** |
pm run build | **Успешно** (exit code 0) | Оптимизированные бандлы, чанки CSS и JS скомпилированы без ошибок |
| **Playwright Suite 13** | playwright test e2e/13-* | **32/32 passed (1.5m)** (exit code 0) | 8 вьюпортов x 4 сценария, включая интерактивный обмен сообщениями и навигацию |
| **Visual QA Capture** | Playwright artifacts | **24 скриншота + manifest.json** | Зафиксированы все 8 вьюпортов во всех 3 темах оформления V3 |
---
## 5. Документация и артефакты
- VISUAL_QA.md: полный протокол визуального аудита и матрица скриншотов.
- rtifacts/visual-qa/: скриншоты всех экранов и manifest.json.
- UI_LIBRARY_USAGE.md: актуализированное руководство по использованию библиотеки @krona/ui и дизайн-токенов V3.
- COMMUNICATIONS_HANDOFF.md: спецификация передачи модуля в эксплуатацию.

View file

@ -1,64 +1,92 @@
# KRONA Visual QA Audit Report — Design System V3 Correction Pass # KRONA Visual QA Audit Report — Mobile & Tablet Correction Pass
**Модуль:** Коммуникации (\/communications\) **Модуль:** Коммуникации (`/communications`)
**Дата проведения аудита:** 11 сентября 2026 г. **Дата проведения аудита:** 11 сентября 2026 г.
**Ветка:** \codex/design-system-communications-correction\ **Рабочая ветка:** `codex/design-system-communications-mobile-tablet-correction`
**Статус проверки:** APPROVED (FAIL-CLOSED VERIFIED) **Базовый коммит:** `49f5a34`
**Статус проверки:** `READY FOR INDEPENDENT REVIEW`
--- ---
## 1. Соответствие эталонным макетам ## 1. Матрица устранения дефектов (Defect Resolution Matrix)
| # | Дефект предыдущей реализации | Решение в текущей ветке (`codex/design-system-communications-mobile-tablet-correction`) | Статус проверки |
|---|-----------------------------|-----------------------------------------------------------------------------------------|:---------------:|
| 1 | **Мобильный интерфейс (< 600px):** отображение нескольких секций одновременно приводило к сжатию и нехватке места. | Реализован строгий **1-колоночный 3-уровневый drilldown**: **Уровень 1** (Список чатов) → **Уровень 2** (Активный чат) → **Уровень 3** (Полная карточка контакта). На экране виден ровно один уровень. Переходы осуществляются через клик по диалогу, тизер профиля и кнопки «Назад». | **PASS** |
| 2 | **Планшет (600–949px):** сплющенная горизонтальная карточка контакта под окном чата сжимала переписку и ломала верстку. | Горизонтальная карточка под чатом **полностью удалена**. Реализован 2-колоночный макет: колонка со списком диалогов (~280–300px) + колонка активного чата (все оставшееся пространство, min 320px). Карточка контакта открывается по кнопке в хедере чата через боковой слайд-овер `Drawer` с полным набором данных и CRM-действиями. | **PASS** |
| 3 | **Десктоп (≥ 950px):** узкие колонки при фиксированных брейкпоинтах окна. | Переход на **3 независимые колонки по ширине рабочей области**: Список диалогов (310–330px) + Активный чат (min 360px) + Карточка контакта (320–350px). При открытии боковой панели Krona AI карточки пропорционально масштабируются без горизонтальной прокрутки. | **PASS** |
| 4 | **Аватары:** использование плейсхолдеров и инициалов вместо визуальных портретов. | Созданы и интегрированы 11 векторных локальных SVG-портретов в `public/avatars/*.svg` с уникальной цветовой дифференциацией. Отображаются в списках, чате, тизере и карточке контакта. | **PASS** |
| 5 | **Соблюдение дизайн-системы V3:** использование сырых HEX-кодов в стилях. | Код полностью переведен на семантические CSS-переменные дизайн-системы V3 (`var(--krona-primary)`, `var(--krona-surface)`, `var(--krona-primary-soft)`, `var(--krona-border)`, `var(--krona-gold)`). Сырые HEX-значения устранены. | **PASS** |
| 6 | **Вкладки и кнопки на мобильных устройствах:** перенос строк и наплыв кнопок. | Вкладки размещены в единую строку с горизонтальным скроллом и градиентным индикатором края. На мобильных кнопка создания контакта/канала сворачивается в компактную touch-friendly иконку 40×40px. | **PASS** |
| 7 | **Наложение мобильной нижней навигации:** инпут чата и тизер профиля перекрывались нижним баром `BottomNav`. | Для страницы `/communications` на мобильных экранах добавлен нижний отступ `pb-16`, гарантирующий свободный доступ ко всем интерактивным элементам без блокировки `BottomNav`. | **PASS** |
---
## 2. Соответствие эталонным макетам
| Эталонный макет | Разрешение / Viewport | Ключевые визуальные элементы | Статус | | Эталонный макет | Разрешение / Viewport | Ключевые визуальные элементы | Статус |
| :--- | :--- | :--- | :---: | | :--- | :--- | :--- | :---: |
| \Коммуникация.png\ | **Desktop (1280, 1440, 1920)** | 3 приподнятые карточки со скруглением ounded-2xl\ на теплом пергаменте; ботанический водяной знак-гравюра и слоган *«Больше, чем общение с поддержкой»*; входящие белые пузыри с аватаркой; исходящие оливково-шалфейные (\#E8EDE4\ / \#2F3A32\) пузыри с двойной галочкой; карточка PDF-вложения; чипсы быстрых ответов; бронзово-коньячная кнопка отправки (\#9A743D\); вертикальная карточка контакта (колонка 3) со сводкой AI, статусом VIP и быстрыми кнопками CRM | PASS | | `Коммуникация.png` | **Desktop (1280, 1440, 1920)** | 3 приподнятые карточки (`rounded-2xl`) на пергаментном фоне; водяной знак и слоган *«Больше, чем общение с поддержкой»*; входящие светлые пузыри с аватаром; исходящие пузыри с двойной галочкой; карточка PDF-вложения; чипсы быстрых ответов; бронзово-золотая кнопка отправки; вертикальная карточка контакта справа (колонка 3) с AI-сводкой, статусом VIP и CRM-действиями. | **PASS** |
| \Коммуникация планшет горизонт.png\ | **Tablet Landscape (1024x768)** | 2-колоночный адаптивный макет: левая колонка со списком диалогов, правая — активный чат сверху и горизонтальная карточка контакта снизу с бейджем VIP, каналами связи и CRM-действиями | PASS | | `Коммуникация планшет горизонт.png` | **Tablet Landscape (1024x768)** | 3-колоночный или просторный 2-колоночный макет с полноразмерным чатом без горизонтального сжатия. | **PASS** |
| \Коммуникация планшет вертикальный.png\ | **Tablet Vertical (768x1024)** | 2-колоночный вертикальный стек: список диалогов слева (300–320px), активный диалог + горизонтальная карточка контакта справа без горизонтального скролла | PASS | | `Коммуникация планшет вертикальный.png` | **Tablet Vertical (768x1024)** | 2-колоночный макет: список диалогов слева (~280-300px), активный диалог справа. Карточка контакта открывается через правый `Drawer`. | **PASS** |
| \Коммуникация телефон.png\ | **Mobile (360, 390, 430)** | 1-колоночный 3-уровневый drilldown: **Уровень 1** (список диалогов) -> **Уровень 2** (активный диалог с нижней тизер-плашкой профиля) -> **Уровень 3** (полная карточка контакта со сделками/задачами); фиксированный инпут над нижней навигацией (\pb-16\ / touch targets >= 44x44px) | PASS | | `Коммуникация телефон.png` | **Mobile (360, 390, 430)** | 1-колоночный drilldown: **Уровень 1** (список диалогов) → **Уровень 2** (активный диалог с нижней плашкой контакта) → **Уровень 3** (карточка контакта); фиксированный инпут над навигацией (`pb-16` / touch targets ≥ 44×44px). | **PASS** |
--- ---
## 2. Матрица скриншотов (8 Viewports x 3 Themes = 24 Screenshots) ## 3. Матрица скриншотов (8 Viewports × 3 Themes = 24 Screenshots)
Все скриншоты сохранены в директории \rtifacts/visual-qa/\ с метаданными в \rtifacts/visual-qa/manifest.json\: Все скриншоты сохранены в директории `artifacts/visual-qa/` с манифестом `artifacts/visual-qa/manifest.json`:
### Parchment Light (Светлый пергамент — основная тема V3) ### 3.1. Parchment Light (Светлый пергамент — основная тема V3)
1. \rtifacts/visual-qa/parchment-light-mobile-360.png\ (360x800) 1. [parchment-light-mobile-360.png](artifacts/visual-qa/parchment-light-mobile-360.png) (360×800)
2. \rtifacts/visual-qa/parchment-light-mobile-390.png\ (390x844) 2. [parchment-light-mobile-390.png](artifacts/visual-qa/parchment-light-mobile-390.png) (390×844)
3. \rtifacts/visual-qa/parchment-light-mobile-430.png\ (430x932) 3. [parchment-light-mobile-430.png](artifacts/visual-qa/parchment-light-mobile-430.png) (430×932)
4. \rtifacts/visual-qa/parchment-light-tablet-768.png\ (768x1024) 4. [parchment-light-tablet-768.png](artifacts/visual-qa/parchment-light-tablet-768.png) (768×1024)
5. \rtifacts/visual-qa/parchment-light-tablet-1024.png\ (1024x768) 5. [parchment-light-tablet-1024.png](artifacts/visual-qa/parchment-light-tablet-1024.png) (1024×768)
6. \rtifacts/visual-qa/parchment-light-desktop-1280.png\ (1280x800) 6. [parchment-light-desktop-1280.png](artifacts/visual-qa/parchment-light-desktop-1280.png) (1280×800)
7. \rtifacts/visual-qa/parchment-light-desktop-1440.png\ (1440x900) 7. [parchment-light-desktop-1440.png](artifacts/visual-qa/parchment-light-desktop-1440.png) (1440×900)
8. \rtifacts/visual-qa/parchment-light-desktop-1920.png\ (1920x1080) 8. [parchment-light-desktop-1920.png](artifacts/visual-qa/parchment-light-desktop-1920.png) (1920×1080)
### Noble Green (Благородный зеленый / мягкая тема V3) ### 3.2. Noble Green (Благородный зелёный / фирменная тема V3)
1. \rtifacts/visual-qa/noble-green-mobile-360.png\ (360x800) 1. [noble-green-mobile-360.png](artifacts/visual-qa/noble-green-mobile-360.png) (360×800)
2. \rtifacts/visual-qa/noble-green-mobile-390.png\ (390x844) 2. [noble-green-mobile-390.png](artifacts/visual-qa/noble-green-mobile-390.png) (390×844)
3. \rtifacts/visual-qa/noble-green-mobile-430.png\ (430x932) 3. [noble-green-mobile-430.png](artifacts/visual-qa/noble-green-mobile-430.png) (430×932)
4. \rtifacts/visual-qa/noble-green-tablet-768.png\ (768x1024) 4. [noble-green-tablet-768.png](artifacts/visual-qa/noble-green-tablet-768.png) (768×1024)
5. \rtifacts/visual-qa/noble-green-tablet-1024.png\ (1024x768) 5. [noble-green-tablet-1024.png](artifacts/visual-qa/noble-green-tablet-1024.png) (1024×768)
6. \rtifacts/visual-qa/noble-green-desktop-1280.png\ (1280x800) 6. [noble-green-desktop-1280.png](artifacts/visual-qa/noble-green-desktop-1280.png) (1280×800)
7. \rtifacts/visual-qa/noble-green-desktop-1440.png\ (1440x900) 7. [noble-green-desktop-1440.png](artifacts/visual-qa/noble-green-desktop-1440.png) (1440×900)
8. \rtifacts/visual-qa/noble-green-desktop-1920.png\ (1920x1080) 8. [noble-green-desktop-1920.png](artifacts/visual-qa/noble-green-desktop-1920.png) (1920×1080)
### Graphite Forest Dark (Тёмная графитово-лесная тема V3) ### 3.3. Graphite Forest Dark (Тёмная графитово-лесная тема V3)
1. \rtifacts/visual-qa/graphite-forest-dark-mobile-360.png\ (360x800) 1. [graphite-forest-dark-mobile-360.png](artifacts/visual-qa/graphite-forest-dark-mobile-360.png) (360×800)
2. \rtifacts/visual-qa/graphite-forest-dark-mobile-390.png\ (390x844) 2. [graphite-forest-dark-mobile-390.png](artifacts/visual-qa/graphite-forest-dark-mobile-390.png) (390×844)
3. \rtifacts/visual-qa/graphite-forest-dark-mobile-430.png\ (430x932) 3. [graphite-forest-dark-mobile-430.png](artifacts/visual-qa/graphite-forest-dark-mobile-430.png) (430×932)
4. \rtifacts/visual-qa/graphite-forest-dark-tablet-768.png\ (768x1024) 4. [graphite-forest-dark-tablet-768.png](artifacts/visual-qa/graphite-forest-dark-tablet-768.png) (768×1024)
5. \rtifacts/visual-qa/graphite-forest-dark-tablet-1024.png\ (1024x768) 5. [graphite-forest-dark-tablet-1024.png](artifacts/visual-qa/graphite-forest-dark-tablet-1024.png) (1024×768)
6. \rtifacts/visual-qa/graphite-forest-dark-desktop-1280.png\ (1280x800) 6. [graphite-forest-dark-desktop-1280.png](artifacts/visual-qa/graphite-forest-dark-desktop-1280.png) (1280×800)
7. \rtifacts/visual-qa/graphite-forest-dark-desktop-1440.png\ (1440x900) 7. [graphite-forest-dark-desktop-1440.png](artifacts/visual-qa/graphite-forest-dark-desktop-1440.png) (1440×900)
8. \rtifacts/visual-qa/graphite-forest-dark-desktop-1920.png\ (1920x1080) 8. [graphite-forest-dark-desktop-1920.png](artifacts/visual-qa/graphite-forest-dark-desktop-1920.png) (1920×1080)
--- ---
## 3. Проверенные интерактивные сценарии ## 4. Скриншоты состояний (State Captures — 7 скриншотов)
1. **Отправка сообщений:** ввод текста в инпут, отправка кнопкой или по Enter, появление исходящего пузыря оливкового цвета со статусом прочтения (двойная галочка). 1. **Мобильный уровень 1 (Список диалогов):** [mobile-list.png](artifacts/visual-qa/mobile-list.png) — отображает только список чатов, строку поиска, быстрые фильтры и пилюли каналов.
2. **Шаблоны и быстрые ответы:** клик по чипсу («Да, подготовим» и др.) мгновенно вставляет и отправляет сообщение. 2. **Мобильный уровень 2 (Активный диалог):** [mobile-chat.png](artifacts/visual-qa/mobile-chat.png) — окно переписки с кнопкой возврата назад, сообщениями, полем ввода и нижней плашкой-тизером контакта.
3. **AI-ассистент:** кнопка с иконкой Sparkles генерирует контекстный черновик сообщения. 3. **Мобильный уровень 3 (Карточка контакта):** [mobile-contact.png](artifacts/visual-qa/mobile-contact.png) — развернутая карточка контакта со сделками CRM, задачами, AI-анализом и кнопкой «Назад к диалогу».
4. **CRM-интеграция:** создание сделки и задачи прямо из активного диалога и карточки контакта с отображением тоста. 4. **Планшет (Активный диалог):** [tablet-chat.png](artifacts/visual-qa/tablet-chat.png) — 2-колоночный вид: список чатов + полноразмерный диалог.
5. **Вкладки раздела:** переход по табам *Мессенджеры*, *Email*, *Каналы*, *Контакты*, *Настройки* без перезагрузки и с синхронизацией URL query parameter \?tab=\. 5. **Планшет (Боковой Drawer контакта):** [tablet-contact-drawer.png](artifacts/visual-qa/tablet-contact-drawer.png) — открытый слайд-овер `Drawer` с профилем клиента поверх чата.
6. **Отсутствие горизонтальной прокрутки:** проверено на всех разрешениях от 360px до 1920px (\scrollWidth <= clientWidth\). 6. **Десктоп (3 колонки):** [desktop-without-krona-ai.png](artifacts/visual-qa/desktop-without-krona-ai.png) — 3 полноценные карточки без боковой панели AI.
7. **Десктоп с открытым Krona AI:** [desktop-with-krona-ai.png](artifacts/visual-qa/desktop-with-krona-ai.png) — сохранение целостности 3 колонок при выдвинутой панели оркестратора Krona AI.
---
## 5. Результаты автоматизированных детерминированных проверок
| Тип проверки | Команда | Результат | Подробности |
|---|---|:---:|---|
| **E2E Visual & Responsive** | `npx playwright test e2e/13-communications-visual-and-responsive.spec.ts` | **PASS (32/32)** | Все 8 проектов (360, 390, 430, 768, 1024, 1280, 1440, 1920) прошли без ошибок (1.7m). |
| **Unit & Integration** | `npm test` | **PASS (490/490)** | 27 тестовых файлов, включая модуль коммуникаций, без сбоев (45.8s). |
| **Linter** | `npm run lint` | **PASS (0/0)** | 0 warnings, 0 errors на 238 файлах (`oxlint --deny-warnings`). |
| **Type Check** | `npx tsc -b` | **PASS** | Exit code 0, строгая типизация TypeScript. |
| **Production Build** | `npm run build` | **PASS** | Сборка Vite завершена успешно за 4.39s. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View file

@ -1,5 +1,5 @@
{ {
"generatedAt": "2026-09-11T10:43:18.547Z", "generatedAt": "2026-09-11T12:25:16.253Z",
"module": "communications", "module": "communications",
"version": "v3.0", "version": "v3.0",
"referenceMockups": [ "referenceMockups": [
@ -64,149 +64,191 @@
} }
], ],
"screenshots": [ "screenshots": [
{
"filename": "desktop-with-krona-ai.png",
"theme": "state-capture",
"viewport": "desktop-with-krona-ai",
"capturedAt": "2026-09-11T12:22:13.311Z"
},
{
"filename": "desktop-without-krona-ai.png",
"theme": "state-capture",
"viewport": "desktop-without-krona-ai",
"capturedAt": "2026-09-11T12:22:12.416Z"
},
{ {
"filename": "graphite-forest-dark-desktop-1280.png", "filename": "graphite-forest-dark-desktop-1280.png",
"theme": "graphite-forest-dark", "theme": "graphite-forest-dark",
"viewport": "desktop-1280", "viewport": "desktop-1280",
"capturedAt": "2026-09-11T10:42:51.480Z" "capturedAt": "2026-09-11T12:21:52.645Z"
}, },
{ {
"filename": "graphite-forest-dark-desktop-1440.png", "filename": "graphite-forest-dark-desktop-1440.png",
"theme": "graphite-forest-dark", "theme": "graphite-forest-dark",
"viewport": "desktop-1440", "viewport": "desktop-1440",
"capturedAt": "2026-09-11T10:43:01.405Z" "capturedAt": "2026-09-11T12:22:07.332Z"
}, },
{ {
"filename": "graphite-forest-dark-desktop-1920.png", "filename": "graphite-forest-dark-desktop-1920.png",
"theme": "graphite-forest-dark", "theme": "graphite-forest-dark",
"viewport": "desktop-1920", "viewport": "desktop-1920",
"capturedAt": "2026-09-11T10:43:12.260Z" "capturedAt": "2026-09-11T12:22:22.152Z"
}, },
{ {
"filename": "graphite-forest-dark-mobile-360.png", "filename": "graphite-forest-dark-mobile-360.png",
"theme": "graphite-forest-dark", "theme": "graphite-forest-dark",
"viewport": "mobile-360", "viewport": "mobile-360",
"capturedAt": "2026-09-11T10:41:51.625Z" "capturedAt": "2026-09-11T12:20:51.014Z"
}, },
{ {
"filename": "graphite-forest-dark-mobile-390.png", "filename": "graphite-forest-dark-mobile-390.png",
"theme": "graphite-forest-dark", "theme": "graphite-forest-dark",
"viewport": "mobile-390", "viewport": "mobile-390",
"capturedAt": "2026-09-11T10:42:04.608Z" "capturedAt": "2026-09-11T12:21:02.940Z"
}, },
{ {
"filename": "graphite-forest-dark-mobile-430.png", "filename": "graphite-forest-dark-mobile-430.png",
"theme": "graphite-forest-dark", "theme": "graphite-forest-dark",
"viewport": "mobile-430", "viewport": "mobile-430",
"capturedAt": "2026-09-11T10:42:15.717Z" "capturedAt": "2026-09-11T12:21:15.107Z"
}, },
{ {
"filename": "graphite-forest-dark-tablet-1024.png", "filename": "graphite-forest-dark-tablet-1024.png",
"theme": "graphite-forest-dark", "theme": "graphite-forest-dark",
"viewport": "tablet-1024", "viewport": "tablet-1024",
"capturedAt": "2026-09-11T10:42:41.564Z" "capturedAt": "2026-09-11T12:21:40.394Z"
}, },
{ {
"filename": "graphite-forest-dark-tablet-768.png", "filename": "graphite-forest-dark-tablet-768.png",
"theme": "graphite-forest-dark", "theme": "graphite-forest-dark",
"viewport": "tablet-768", "viewport": "tablet-768",
"capturedAt": "2026-09-11T10:42:27.167Z" "capturedAt": "2026-09-11T12:21:27.469Z"
},
{
"filename": "mobile-chat.png",
"theme": "state-capture",
"viewport": "mobile-chat",
"capturedAt": "2026-09-11T12:25:15.761Z"
},
{
"filename": "mobile-contact.png",
"theme": "state-capture",
"viewport": "mobile-contact",
"capturedAt": "2026-09-11T12:25:15.982Z"
},
{
"filename": "mobile-list.png",
"theme": "state-capture",
"viewport": "mobile-list",
"capturedAt": "2026-09-11T12:25:15.531Z"
}, },
{ {
"filename": "noble-green-desktop-1280.png", "filename": "noble-green-desktop-1280.png",
"theme": "noble-green", "theme": "noble-green",
"viewport": "desktop-1280", "viewport": "desktop-1280",
"capturedAt": "2026-09-11T10:42:51.030Z" "capturedAt": "2026-09-11T12:21:52.093Z"
}, },
{ {
"filename": "noble-green-desktop-1440.png", "filename": "noble-green-desktop-1440.png",
"theme": "noble-green", "theme": "noble-green",
"viewport": "desktop-1440", "viewport": "desktop-1440",
"capturedAt": "2026-09-11T10:43:00.944Z" "capturedAt": "2026-09-11T12:22:06.701Z"
}, },
{ {
"filename": "noble-green-desktop-1920.png", "filename": "noble-green-desktop-1920.png",
"theme": "noble-green", "theme": "noble-green",
"viewport": "desktop-1920", "viewport": "desktop-1920",
"capturedAt": "2026-09-11T10:43:11.630Z" "capturedAt": "2026-09-11T12:22:21.509Z"
}, },
{ {
"filename": "noble-green-mobile-360.png", "filename": "noble-green-mobile-360.png",
"theme": "noble-green", "theme": "noble-green",
"viewport": "mobile-360", "viewport": "mobile-360",
"capturedAt": "2026-09-11T10:41:51.173Z" "capturedAt": "2026-09-11T12:20:50.611Z"
}, },
{ {
"filename": "noble-green-mobile-390.png", "filename": "noble-green-mobile-390.png",
"theme": "noble-green", "theme": "noble-green",
"viewport": "mobile-390", "viewport": "mobile-390",
"capturedAt": "2026-09-11T10:42:04.141Z" "capturedAt": "2026-09-11T12:21:02.534Z"
}, },
{ {
"filename": "noble-green-mobile-430.png", "filename": "noble-green-mobile-430.png",
"theme": "noble-green", "theme": "noble-green",
"viewport": "mobile-430", "viewport": "mobile-430",
"capturedAt": "2026-09-11T10:42:15.277Z" "capturedAt": "2026-09-11T12:21:14.716Z"
}, },
{ {
"filename": "noble-green-tablet-1024.png", "filename": "noble-green-tablet-1024.png",
"theme": "noble-green", "theme": "noble-green",
"viewport": "tablet-1024", "viewport": "tablet-1024",
"capturedAt": "2026-09-11T10:42:41.113Z" "capturedAt": "2026-09-11T12:21:39.851Z"
}, },
{ {
"filename": "noble-green-tablet-768.png", "filename": "noble-green-tablet-768.png",
"theme": "noble-green", "theme": "noble-green",
"viewport": "tablet-768", "viewport": "tablet-768",
"capturedAt": "2026-09-11T10:42:26.644Z" "capturedAt": "2026-09-11T12:21:26.941Z"
}, },
{ {
"filename": "parchment-light-desktop-1280.png", "filename": "parchment-light-desktop-1280.png",
"theme": "parchment-light", "theme": "parchment-light",
"viewport": "desktop-1280", "viewport": "desktop-1280",
"capturedAt": "2026-09-11T10:42:50.501Z" "capturedAt": "2026-09-11T12:21:51.543Z"
}, },
{ {
"filename": "parchment-light-desktop-1440.png", "filename": "parchment-light-desktop-1440.png",
"theme": "parchment-light", "theme": "parchment-light",
"viewport": "desktop-1440", "viewport": "desktop-1440",
"capturedAt": "2026-09-11T10:43:00.457Z" "capturedAt": "2026-09-11T12:22:06.120Z"
}, },
{ {
"filename": "parchment-light-desktop-1920.png", "filename": "parchment-light-desktop-1920.png",
"theme": "parchment-light", "theme": "parchment-light",
"viewport": "desktop-1920", "viewport": "desktop-1920",
"capturedAt": "2026-09-11T10:43:10.935Z" "capturedAt": "2026-09-11T12:22:20.870Z"
}, },
{ {
"filename": "parchment-light-mobile-360.png", "filename": "parchment-light-mobile-360.png",
"theme": "parchment-light", "theme": "parchment-light",
"viewport": "mobile-360", "viewport": "mobile-360",
"capturedAt": "2026-09-11T10:41:50.743Z" "capturedAt": "2026-09-11T12:20:50.223Z"
}, },
{ {
"filename": "parchment-light-mobile-390.png", "filename": "parchment-light-mobile-390.png",
"theme": "parchment-light", "theme": "parchment-light",
"viewport": "mobile-390", "viewport": "mobile-390",
"capturedAt": "2026-09-11T10:42:03.687Z" "capturedAt": "2026-09-11T12:21:02.124Z"
}, },
{ {
"filename": "parchment-light-mobile-430.png", "filename": "parchment-light-mobile-430.png",
"theme": "parchment-light", "theme": "parchment-light",
"viewport": "mobile-430", "viewport": "mobile-430",
"capturedAt": "2026-09-11T10:42:14.847Z" "capturedAt": "2026-09-11T12:21:14.326Z"
}, },
{ {
"filename": "parchment-light-tablet-1024.png", "filename": "parchment-light-tablet-1024.png",
"theme": "parchment-light", "theme": "parchment-light",
"viewport": "tablet-1024", "viewport": "tablet-1024",
"capturedAt": "2026-09-11T10:42:40.633Z" "capturedAt": "2026-09-11T12:21:39.371Z"
}, },
{ {
"filename": "parchment-light-tablet-768.png", "filename": "parchment-light-tablet-768.png",
"theme": "parchment-light", "theme": "parchment-light",
"viewport": "tablet-768", "viewport": "tablet-768",
"capturedAt": "2026-09-11T10:42:26.175Z" "capturedAt": "2026-09-11T12:21:26.400Z"
},
{
"filename": "tablet-chat.png",
"theme": "state-capture",
"viewport": "tablet-chat",
"capturedAt": "2026-09-11T12:21:31.840Z"
},
{
"filename": "tablet-contact-drawer.png",
"theme": "state-capture",
"viewport": "tablet-contact-drawer",
"capturedAt": "2026-09-11T12:21:32.462Z"
} }
] ]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

View file

@ -6,6 +6,7 @@ const THEMES = ['parchment-light', 'noble-green', 'graphite-forest-dark'] as con
test.describe('Scenario 13: Communications Visual & Responsive Design System V3 Suite', () => { test.describe('Scenario 13: Communications Visual & Responsive Design System V3 Suite', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
test.setTimeout(60000);
// Navigate directly to Communications module // Navigate directly to Communications module
await page.goto('/communications', { waitUntil: 'domcontentloaded' }); await page.goto('/communications', { waitUntil: 'domcontentloaded' });
await expect(page.locator('h1', { hasText: 'Коммуникации' })).toBeVisible({ timeout: 15000 }); await expect(page.locator('h1', { hasText: 'Коммуникации' })).toBeVisible({ timeout: 15000 });
@ -43,11 +44,22 @@ test.describe('Scenario 13: Communications Visual & Responsive Design System V3
}); });
test('2. Header, Slogan Watermark, and Channel Switcher validation', async ({ page }) => { test('2. Header, Slogan Watermark, and Channel Switcher validation', async ({ page }) => {
const vp = page.viewportSize();
const isMobile = (vp?.width ?? 1200) < 640;
const isDesktop = (vp?.width ?? 1200) >= 1024;
// Check main header // Check main header
await expect(page.locator('h1', { hasText: 'Коммуникации' })).toBeVisible(); await expect(page.locator('h1', { hasText: 'Коммуникации' })).toBeVisible();
if (!isMobile) {
await expect( await expect(
page.locator('text=Все сообщения, почта и каналы компании в одном пространстве') page.locator('text=Все сообщения, почта и каналы компании в одном пространстве')
).toBeVisible(); ).toBeVisible();
}
if (isDesktop) {
await expect(page.locator('text=Больше, чем общение с поддержкой')).toBeVisible();
}
// Check all 5 tabs // Check all 5 tabs
await expect(page.getByRole('tab', { name: /мессенджеры/i })).toBeVisible(); await expect(page.getByRole('tab', { name: /мессенджеры/i })).toBeVisible();
@ -63,19 +75,49 @@ test.describe('Scenario 13: Communications Visual & Responsive Design System V3
await expect(page.getByRole('button', { name: /vk.*6/i })).toBeVisible(); await expect(page.getByRole('button', { name: /vk.*6/i })).toBeVisible();
await expect(page.getByRole('button', { name: /max.*4/i })).toBeVisible(); await expect(page.getByRole('button', { name: /max.*4/i })).toBeVisible();
await expect(page.getByRole('button', { name: /email.*10/i })).toBeVisible(); await expect(page.getByRole('button', { name: /email.*10/i })).toBeVisible();
await expect(page.getByRole('button', { name: /подключить/i })).toBeVisible();
});
test('3. Responsive layout behavior and drill-down flow', async ({ page, viewport }) => {
const isMobile = viewport && viewport.width < 768;
const isTablet = viewport && viewport.width >= 768 && viewport.width < 1280;
const isDesktop = viewport && viewport.width >= 1280;
if (isMobile) { if (isMobile) {
// Mobile drilldown check: Level 1 (List) -> Level 2 (Chat) -> Level 3 (Contact) await expect(page.getByRole('button', { name: /подключить канал/i })).toBeVisible();
} else {
await expect(page.getByRole('button', { name: /подключить/i })).toBeVisible();
}
});
test('3. Responsive layout behavior, geometric assertions and state screenshots', async ({
page,
}, testInfo) => {
const projectName = testInfo.project.name;
const artifactsDir = path.resolve(process.cwd(), 'artifacts/visual-qa');
if (!fs.existsSync(artifactsDir)) {
fs.mkdirSync(artifactsDir, { recursive: true });
}
const layoutMode = await page.locator('[data-layout-mode]').getAttribute('data-layout-mode');
const isMobile = layoutMode === 'compact';
const isTablet = layoutMode === 'medium';
const isDesktop = layoutMode === 'wide';
if (isMobile) {
// 1. Mobile drilldown check: Level 1 (List) -> Level 2 (Chat) -> Level 3 (Contact)
const chatList = page.locator('section[aria-label="Список диалогов"]'); const chatList = page.locator('section[aria-label="Список диалогов"]');
await expect(chatList).toBeVisible(); await expect(chatList).toBeVisible();
// State Screenshot 1: Mobile List
if (projectName === 'mobile-390' || projectName === 'mobile-360') {
await page.screenshot({
path: path.join(artifactsDir, 'mobile-list.png'),
fullPage: false,
});
}
// Check single-row tabs with scroll
const tabsList = page.locator('div[role="tablist"]');
const tabsBox = await tabsList.boundingBox();
expect(tabsBox).not.toBeNull();
if (tabsBox) {
expect(tabsBox.height).toBeLessThanOrEqual(56);
}
// Click first conversation to drill down to Level 2 (Chat) // Click first conversation to drill down to Level 2 (Chat)
const firstChat = chatList.locator('button').filter({ hasText: 'Иван Петров' }).first(); const firstChat = chatList.locator('button').filter({ hasText: 'Иван Петров' }).first();
await firstChat.click(); await firstChat.click();
@ -83,6 +125,16 @@ test.describe('Scenario 13: Communications Visual & Responsive Design System V3
// Active chat is now visible // Active chat is now visible
const activeChat = page.locator('section[aria-label="Активный диалог"]'); const activeChat = page.locator('section[aria-label="Активный диалог"]');
await expect(activeChat).toBeVisible(); await expect(activeChat).toBeVisible();
// Chat list is NOT visible in compact mode
await expect(chatList).toBeHidden();
// State Screenshot 2: Mobile Chat
if (projectName === 'mobile-390' || projectName === 'mobile-360') {
await page.screenshot({
path: path.join(artifactsDir, 'mobile-chat.png'),
fullPage: false,
});
}
// Check Back to list button // Check Back to list button
const backToListBtn = activeChat.locator('button[aria-label="Назад к списку чатов"]'); const backToListBtn = activeChat.locator('button[aria-label="Назад к списку чатов"]');
@ -92,10 +144,28 @@ test.describe('Scenario 13: Communications Visual & Responsive Design System V3
const profileTeaserBtn = activeChat.locator('button[aria-label="Открыть карточку контакта"]'); const profileTeaserBtn = activeChat.locator('button[aria-label="Открыть карточку контакта"]');
await expect(profileTeaserBtn).toBeVisible(); await expect(profileTeaserBtn).toBeVisible();
// Check message composer is positioned within viewport
const composer = activeChat.locator('form');
await expect(composer).toBeVisible();
const composerBox = await composer.boundingBox();
const vp = page.viewportSize();
if (composerBox && vp) {
expect(composerBox.y + composerBox.height).toBeLessThanOrEqual(vp.height + 10);
}
// Click profile teaser to drill down to Level 3 (Contact) // Click profile teaser to drill down to Level 3 (Contact)
await profileTeaserBtn.click(); await profileTeaserBtn.click();
const mobileContactCard = page.locator('section[aria-label="Карточка контакта"]'); const mobileContactCard = page.locator('section[aria-label="Карточка контакта"]');
await expect(mobileContactCard).toBeVisible(); await expect(mobileContactCard).toBeVisible();
await expect(activeChat).toBeHidden();
// State Screenshot 3: Mobile Contact
if (projectName === 'mobile-390' || projectName === 'mobile-360') {
await page.screenshot({
path: path.join(artifactsDir, 'mobile-contact.png'),
fullPage: false,
});
}
// Check Back to chat button in Level 3 // Check Back to chat button in Level 3
const backToChatBtn = mobileContactCard.locator('button[aria-label="Назад к диалогу"]'); const backToChatBtn = mobileContactCard.locator('button[aria-label="Назад к диалогу"]');
@ -115,18 +185,62 @@ test.describe('Scenario 13: Communications Visual & Responsive Design System V3
}); });
expect(hasHorizontalScroll).toBe(false); expect(hasHorizontalScroll).toBe(false);
} else if (isTablet) { } else if (isTablet) {
// Tablet (768 vertical and 1024 landscape): 2 columns (Chat list on left, Chat + Horizontal Contact Card on right) // 2. Tablet: 2 columns (Chat list + Active Chat). Contact card opens via slide-over Drawer.
const chatList = page.locator('section[aria-label="Список диалогов"]'); const chatList = page.locator('section[aria-label="Список диалогов"]');
await expect(chatList).toBeVisible(); await expect(chatList).toBeVisible();
const activeChat = page.locator('section[aria-label="Активный диалог"]'); const activeChat = page.locator('section[aria-label="Активный диалог"]');
await expect(activeChat).toBeVisible(); await expect(activeChat).toBeVisible();
const horizontalContactCard = page.locator('aside[aria-label="Карточка контакта планшет"]'); // Verify Chat width >= 320px
await expect(horizontalContactCard).toBeVisible(); const chatBox = await activeChat.boundingBox();
await expect(horizontalContactCard.locator('text=VIP')).toBeVisible(); expect(chatBox).not.toBeNull();
if (chatBox) {
expect(chatBox.width).toBeGreaterThanOrEqual(320);
}
// Verify no squished under-chat horizontal card exists
const squishedCard = page.locator('aside[aria-label="Карточка контакта планшет"]');
await expect(squishedCard).toHaveCount(0);
// State Screenshot 4: Tablet Chat
if (projectName === 'tablet-768' || projectName === 'tablet-1024') {
await page.screenshot({
path: path.join(artifactsDir, 'tablet-chat.png'),
fullPage: false,
});
}
// Open slide-over Drawer via contact button in chat header
const openContactBtn = activeChat.locator('button[aria-label="Открыть контакт"]');
await expect(openContactBtn).toBeVisible();
await openContactBtn.click();
// Drawer is open
const drawer = page.locator('div[role="dialog"]');
await expect(drawer).toBeVisible();
await expect(drawer.locator('text=Карточка контакта')).toBeVisible();
await expect(drawer.locator('text=Иван Петров').first()).toBeVisible();
await expect(drawer.locator('text=VIP')).toBeVisible();
// State Screenshot 5: Tablet Contact Drawer
if (projectName === 'tablet-768' || projectName === 'tablet-1024') {
await page.screenshot({
path: path.join(artifactsDir, 'tablet-contact-drawer.png'),
fullPage: false,
});
}
// Close Drawer
const closeDrawerBtn = drawer.locator('button[aria-label="Закрыть"]').first();
if (await closeDrawerBtn.isVisible()) {
await closeDrawerBtn.click();
} else {
await page.keyboard.press('Escape');
}
await expect(drawer).toBeHidden();
} else if (isDesktop) { } else if (isDesktop) {
// Desktop: 3 separate elevated columns side-by-side // 3. Desktop: 3 columns (Chat list + Active Chat + Aside Contact card)
const chatList = page.locator('section[aria-label="Список диалогов"]'); const chatList = page.locator('section[aria-label="Список диалогов"]');
await expect(chatList).toBeVisible(); await expect(chatList).toBeVisible();
@ -137,12 +251,48 @@ test.describe('Scenario 13: Communications Visual & Responsive Design System V3
await expect(verticalContactCard).toBeVisible(); await expect(verticalContactCard).toBeVisible();
await expect(verticalContactCard.locator('text=Иван Петров')).toBeVisible(); await expect(verticalContactCard.locator('text=Иван Петров')).toBeVisible();
await expect(verticalContactCard.locator('text=VIP')).toBeVisible(); await expect(verticalContactCard.locator('text=VIP')).toBeVisible();
// Test tab inside Contact Card
await verticalContactCard.getByRole('button', { name: 'AI-сводка' }).click();
await expect(verticalContactCard.locator('text=✦ AI-сводка')).toBeVisible(); await expect(verticalContactCard.locator('text=✦ AI-сводка')).toBeVisible();
// Verify scrollY is 0
const scrollY = await page.evaluate(() => window.scrollY);
expect(scrollY).toBe(0);
// State Screenshot 6: Desktop without Krona AI
if (projectName === 'desktop-1440') {
await page.screenshot({
path: path.join(artifactsDir, 'desktop-without-krona-ai.png'),
fullPage: false,
});
}
// Open Krona AI drawer
const aiBtn = page.locator('button[title="Krona AI — Бизнес-оркестратор"]');
if (await aiBtn.isVisible()) {
await aiBtn.click();
const aiDrawer = page.locator('text=✦ Krona AI — Бизнес-оркестратор');
await expect(aiDrawer).toBeVisible();
// State Screenshot 7: Desktop with Krona AI
if (projectName === 'desktop-1440') {
await page.screenshot({
path: path.join(artifactsDir, 'desktop-with-krona-ai.png'),
fullPage: false,
});
}
// Close AI drawer
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
}
} }
}); });
test('4. Interactive messaging, quick replies, and tab navigation', async ({ page, viewport }) => { test('4. Interactive messaging, quick replies, and tab navigation', async ({ page }) => {
const isMobile = viewport && viewport.width < 768; const layoutMode = await page.locator('[data-layout-mode]').getAttribute('data-layout-mode');
const isMobile = layoutMode === 'compact';
if (isMobile) { if (isMobile) {
// Select chat first on mobile // Select chat first on mobile
@ -233,8 +383,12 @@ test.describe('Scenario 13: Communications Visual & Responsive Design System V3
? 'graphite-forest-dark' ? 'graphite-forest-dark'
: file.startsWith('noble-green') : file.startsWith('noble-green')
? 'noble-green' ? 'noble-green'
: 'parchment-light', : file.startsWith('parchment-light')
viewport: file.replace(/^(graphite-forest-dark|noble-green|parchment-light)-/, '').replace('.png', ''), ? 'parchment-light'
: 'state-capture',
viewport: file
.replace(/^(graphite-forest-dark|noble-green|parchment-light)-/, '')
.replace('.png', ''),
capturedAt: fs.statSync(path.join(artifactsDir, file)).mtime.toISOString(), capturedAt: fs.statSync(path.join(artifactsDir, file)).mtime.toISOString(),
})), })),
}; };

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="32" fill="#E9E5DC"/>
<circle cx="32" cy="24" r="14" fill="#433A30"/>
<path d="M16 52 C16 40, 24 36, 32 36 C40 36, 48 40, 48 52 Z" fill="#433A30"/>
<circle cx="27" cy="23" r="1.8" fill="#E9E5DC"/>
<circle cx="37" cy="23" r="1.8" fill="#E9E5DC"/>
<path d="M28 29 Q32 32 36 29" stroke="#E9E5DC" stroke-width="1.5" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

10
public/avatars/dmitry.svg Normal file
View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="32" fill="#E8E3DA"/>
<circle cx="32" cy="24" r="14" fill="#3B372F"/>
<path d="M16 52 C16 40, 24 36, 32 36 C40 36, 48 40, 48 52 Z" fill="#3B372F"/>
<circle cx="27" cy="23" r="1.8" fill="#E8E3DA"/>
<circle cx="37" cy="23" r="1.8" fill="#E8E3DA"/>
<path d="M28 29 Q32 32 36 29" stroke="#E8E3DA" stroke-width="1.5" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

11
public/avatars/elena.svg Normal file
View file

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="32" fill="#E8EDE6"/>
<path d="M18 26 C18 14, 46 14, 46 26 C46 36, 42 42, 42 42 L22 42 C22 42, 18 36, 18 26 Z" fill="#477057"/>
<circle cx="32" cy="25" r="12" fill="#264233"/>
<path d="M16 54 C16 42, 24 38, 32 38 C40 38, 48 42, 48 54 Z" fill="#264233"/>
<circle cx="28" cy="24" r="1.8" fill="#E8EDE6"/>
<circle cx="36" cy="24" r="1.8" fill="#E8EDE6"/>
<path d="M28 30 Q32 33 36 30" stroke="#E8EDE6" stroke-width="1.5" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 640 B

10
public/avatars/ivan.svg Normal file
View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="32" fill="#EBE3D5"/>
<circle cx="32" cy="24" r="14" fill="#2C3E35"/>
<path d="M16 52 C16 40, 24 36, 32 36 C40 36, 48 40, 48 52 Z" fill="#2C3E35"/>
<circle cx="27" cy="23" r="1.8" fill="#EBE3D5"/>
<circle cx="37" cy="23" r="1.8" fill="#EBE3D5"/>
<path d="M28 29 Q32 32 36 29" stroke="#EBE3D5" stroke-width="1.5" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="32" fill="#E4EBE6"/>
<circle cx="32" cy="24" r="14" fill="#293C32"/>
<path d="M16 52 C16 40, 24 36, 32 36 C40 36, 48 40, 48 52 Z" fill="#293C32"/>
<circle cx="27" cy="23" r="1.8" fill="#E4EBE6"/>
<circle cx="37" cy="23" r="1.8" fill="#E4EBE6"/>
<path d="M28 29 Q32 32 36 29" stroke="#E4EBE6" stroke-width="1.5" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

11
public/avatars/maria.svg Normal file
View file

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="32" fill="#EFE8E1"/>
<path d="M18 26 C18 14, 46 14, 46 26 C46 36, 42 42, 42 42 L22 42 C22 42, 18 36, 18 26 Z" fill="#9A5B3D"/>
<circle cx="32" cy="25" r="12" fill="#51372E"/>
<path d="M16 54 C16 42, 24 38, 32 38 C40 38, 48 42, 48 54 Z" fill="#51372E"/>
<circle cx="28" cy="24" r="1.8" fill="#EFE8E1"/>
<circle cx="36" cy="24" r="1.8" fill="#EFE8E1"/>
<path d="M28 30 Q32 33 36 30" stroke="#EFE8E1" stroke-width="1.5" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 640 B

View file

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="32" fill="#ECE5DE"/>
<rect x="20" y="18" width="24" height="32" rx="3" fill="#48352A"/>
<rect x="24" y="22" width="4" height="4" rx="1" fill="#ECE5DE"/>
<rect x="36" y="22" width="4" height="4" rx="1" fill="#ECE5DE"/>
<rect x="24" y="30" width="4" height="4" rx="1" fill="#ECE5DE"/>
<rect x="36" y="30" width="4" height="4" rx="1" fill="#ECE5DE"/>
<rect x="28" y="40" width="8" height="10" rx="1" fill="#ECE5DE"/>
</svg>

After

Width:  |  Height:  |  Size: 600 B

11
public/avatars/olga.svg Normal file
View file

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="32" fill="#E5EBE3"/>
<path d="M18 26 C18 14, 46 14, 46 26 C46 36, 42 42, 42 42 L22 42 C22 42, 18 36, 18 26 Z" fill="#2B4A3A"/>
<circle cx="32" cy="25" r="12" fill="#32483D"/>
<path d="M16 54 C16 42, 24 38, 32 38 C40 38, 48 42, 48 54 Z" fill="#32483D"/>
<circle cx="28" cy="24" r="1.8" fill="#E5EBE3"/>
<circle cx="36" cy="24" r="1.8" fill="#E5EBE3"/>
<path d="M28 30 Q32 33 36 30" stroke="#E5EBE3" stroke-width="1.5" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 640 B

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="32" fill="#E3EBE6"/>
<circle cx="32" cy="24" r="14" fill="#1E3A2F"/>
<path d="M16 52 C16 40, 24 36, 32 36 C40 36, 48 40, 48 52 Z" fill="#1E3A2F"/>
<circle cx="27" cy="23" r="1.8" fill="#E3EBE6"/>
<circle cx="37" cy="23" r="1.8" fill="#E3EBE6"/>
<path d="M28 29 Q32 32 36 29" stroke="#E3EBE6" stroke-width="1.5" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

11
public/avatars/resurs.svg Normal file
View file

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="32" fill="#DEE5E0"/>
<rect x="20" y="18" width="24" height="32" rx="3" fill="#1F3B2E"/>
<rect x="24" y="22" width="4" height="4" rx="1" fill="#DEE5E0"/>
<rect x="36" y="22" width="4" height="4" rx="1" fill="#DEE5E0"/>
<rect x="24" y="30" width="4" height="4" rx="1" fill="#DEE5E0"/>
<rect x="36" y="30" width="4" height="4" rx="1" fill="#DEE5E0"/>
<rect x="28" y="40" width="8" height="10" rx="1" fill="#DEE5E0"/>
</svg>

After

Width:  |  Height:  |  Size: 600 B

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<rect width="64" height="64" rx="32" fill="#DFE9E3"/>
<circle cx="32" cy="26" r="13" fill="#1B3B2B"/>
<path d="M16 52 C16 42, 24 38, 32 38 C40 38, 48 42, 48 52 Z" fill="#1B3B2B"/>
<path d="M22 24 C22 18, 42 18, 42 24 L42 28 C42 30, 40 31, 38 31" stroke="#276848" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<circle cx="42" cy="28" r="3" fill="#276848"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

View file

@ -28,6 +28,12 @@ export const AppShell: React.FC = () => {
useEffect(() => { useEffect(() => {
document.documentElement.setAttribute('data-theme', theme); document.documentElement.setAttribute('data-theme', theme);
const kronaTheme = {
light: 'parchment-light',
'soft-light': 'noble-green',
dark: 'graphite-forest-dark',
}[theme];
document.documentElement.setAttribute('data-krona-theme', kronaTheme);
}, [theme]); }, [theme]);
// Layout resolution: // Layout resolution:
@ -41,7 +47,7 @@ export const AppShell: React.FC = () => {
const getViewportContainerStyles = () => { const getViewportContainerStyles = () => {
if (!isEmulatedOnDesktop) { if (!isEmulatedOnDesktop) {
return 'w-full min-h-screen'; return isCommunicationsPage ? 'w-full h-screen max-h-screen overflow-hidden' : 'w-full min-h-screen';
} }
switch (viewportMode) { switch (viewportMode) {
case 'mobile': case 'mobile':
@ -51,14 +57,16 @@ export const AppShell: React.FC = () => {
case 'tablet-landscape': case 'tablet-landscape':
return 'max-w-[1180px] h-[800px] my-6 border-8 border-krona-border rounded-[24px] overflow-hidden shadow-2xl relative'; return 'max-w-[1180px] h-[800px] my-6 border-8 border-krona-border rounded-[24px] overflow-hidden shadow-2xl relative';
default: default:
return 'w-full min-h-screen'; return isCommunicationsPage ? 'w-full h-screen max-h-screen overflow-hidden' : 'w-full min-h-screen';
} }
}; };
return ( return (
<div <div
data-testid="app-shell-root" data-testid="app-shell-root"
className={`min-h-screen bg-krona-app text-krona-primary flex items-center justify-center ${ className={`${
isCommunicationsPage ? 'h-screen max-h-screen overflow-hidden' : 'min-h-screen'
} bg-krona-app text-krona-primary flex items-center justify-center ${
isEmulatedOnDesktop ? 'p-4' : '' isEmulatedOnDesktop ? 'p-4' : ''
}`} }`}
> >
@ -76,9 +84,9 @@ export const AppShell: React.FC = () => {
{/* Main Content Router Outlet */} {/* Main Content Router Outlet */}
<main <main
className={`flex-1 ${ className={`flex-1 min-h-0 ${
isCommunicationsPage isCommunicationsPage
? `overflow-hidden flex flex-col min-h-0 p-0 ${isEffectiveMobile ? 'pb-16' : ''}` ? `overflow-hidden flex flex-col pt-0 px-0 ${isEffectiveMobile ? 'pb-20' : 'pb-0'}`
: `overflow-y-auto ${isEffectiveMobile ? 'pb-24' : 'pb-8'} p-3 sm:p-4 md:p-6` : `overflow-y-auto ${isEffectiveMobile ? 'pb-24' : 'pb-8'} p-3 sm:p-4 md:p-6`
}`} }`}
> >

View file

@ -52,7 +52,7 @@ export function CommunicationsHeader({
]; ];
return ( return (
<header className="relative bg-[var(--krona-surface)] border-b border-[var(--krona-border-subtle)] px-4 sm:px-6 pt-5 pb-3"> <header className="relative bg-[var(--krona-surface)] border-b border-[var(--krona-border-subtle)] px-3 sm:px-6 pt-3 sm:pt-5 pb-2 sm:pb-3 shrink-0 overflow-hidden">
<div className="hidden lg:flex items-center gap-3 absolute right-8 top-4 text-right pointer-events-none opacity-50 font-serif italic text-sm text-[var(--krona-text-secondary)]"> <div className="hidden lg:flex items-center gap-3 absolute right-8 top-4 text-right pointer-events-none opacity-50 font-serif italic text-sm text-[var(--krona-text-secondary)]">
<span>Больше, чем общение с поддержкой</span> <span>Больше, чем общение с поддержкой</span>
<svg <svg
@ -75,22 +75,40 @@ export function CommunicationsHeader({
</svg> </svg>
</div> </div>
<div className="mb-4"> <div className="flex items-center justify-between gap-3 mb-2 sm:mb-4">
<h1 className="text-2xl sm:text-3xl font-bold font-serif text-[var(--krona-text-primary)]"> <div>
<h1 className="text-xl sm:text-3xl font-bold font-serif text-[var(--krona-text-primary)]">
Коммуникации Коммуникации
</h1> </h1>
<p className="text-sm text-[var(--krona-text-secondary)] mt-1"> <p className="hidden sm:block text-sm text-[var(--krona-text-secondary)] mt-1">
Все сообщения, почта и каналы компании в одном пространстве Все сообщения, почта и каналы компании в одном пространстве
</p> </p>
</div> </div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-1"> {/* Mobile compact action button */}
<button
type="button"
onClick={onAddChannel}
className="sm:hidden h-10 w-10 rounded-lg border border-[var(--krona-border)] bg-[var(--krona-surface)] hover:bg-[var(--krona-surface-hover)] text-[var(--krona-primary)] font-medium flex items-center justify-center shrink-0 transition-colors active:scale-95"
aria-label="Добавить канал"
>
<Plus className="w-4 h-4 text-[var(--krona-primary)]" />
<span className="sr-only">Добавить канал</span>
</button>
</div>
<div className="flex items-center justify-between gap-3 min-w-0">
<div className="flex-1 min-w-0 overflow-x-auto scrollbar-none">
<Tabs <Tabs
items={tabItems} items={tabItems}
activeId={activeTab} activeId={activeTab}
onChange={(id) => onTabChange(id as CommunicationTab)} onChange={(id) => onTabChange(id as CommunicationTab)}
className="border-b-0 whitespace-nowrap"
/> />
<div className="flex items-center gap-2 self-end sm:self-auto shrink-0"> </div>
{/* Desktop / Tablet full button */}
<div className="hidden sm:flex items-center gap-2 shrink-0">
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,7 @@ export const initialConversations: ChatConversation[] = [
contact: { contact: {
id: 'contact-ivan', id: 'contact-ivan',
name: 'Иван Петров', name: 'Иван Петров',
avatarUrl: '/avatars/ivan.svg',
initials: 'ИП', initials: 'ИП',
company: 'ООО «Альфа»', company: 'ООО «Альфа»',
role: 'Директор', role: 'Директор',
@ -190,6 +191,7 @@ export const initialConversations: ChatConversation[] = [
contact: { contact: {
id: 'contact-maria', id: 'contact-maria',
name: 'Мария К.', name: 'Мария К.',
avatarUrl: '/avatars/maria.svg',
initials: 'МК', initials: 'МК',
company: 'Студия Красоты «Шарм»', company: 'Студия Красоты «Шарм»',
role: 'Управляющая', role: 'Управляющая',
@ -225,6 +227,7 @@ export const initialConversations: ChatConversation[] = [
contact: { contact: {
id: 'contact-resurs', id: 'contact-resurs',
name: 'Поставщик ООО «Ресурс»', name: 'Поставщик ООО «Ресурс»',
avatarUrl: '/avatars/resurs.svg',
initials: 'ПР', initials: 'ПР',
company: 'ООО «Ресурс-Снаб»', company: 'ООО «Ресурс-Снаб»',
role: 'Менеджер по отгрузкам', role: 'Менеджер по отгрузкам',
@ -266,6 +269,7 @@ export const initialConversations: ChatConversation[] = [
contact: { contact: {
id: 'contact-elena', id: 'contact-elena',
name: 'Елена Васильева', name: 'Елена Васильева',
avatarUrl: '/avatars/elena.svg',
initials: 'ЕВ', initials: 'ЕВ',
company: 'Ресторан «Мята»', company: 'Ресторан «Мята»',
role: 'Шеф-повар', role: 'Шеф-повар',
@ -371,6 +375,7 @@ export const initialConversations: ChatConversation[] = [
contact: { contact: {
id: 'contact-support', id: 'contact-support',
name: 'Служба поддержки', name: 'Служба поддержки',
avatarUrl: '/avatars/support.svg',
initials: 'СП', initials: 'СП',
company: 'Krona Platform', company: 'Krona Platform',
role: 'Дежурная смена', role: 'Дежурная смена',

View file

@ -1,5 +1,6 @@
@import '@krona/tokens.css'; @import '@krona/tokens.css';
@import '@krona/themes.css'; @import '@krona/themes.css';
@import '@krona/ui.css';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@ -24,18 +25,18 @@
--surface-selected: var(--krona-surface-selected); --surface-selected: var(--krona-surface-selected);
--surface-disabled: var(--krona-surface-muted); --surface-disabled: var(--krona-surface-muted);
--sidebar-bg: var(--krona-surface); --sidebar-bg: #284D3A;
--sidebar-deep: var(--krona-bg-app); --sidebar-deep: #213F30;
--sidebar-surface: var(--krona-surface-raised); --sidebar-surface: #2D5741;
--sidebar-hover: var(--krona-surface-hover); --sidebar-hover: #356149;
--sidebar-active: var(--krona-surface-selected); --sidebar-active: #3C684F;
--sidebar-border: var(--krona-border-subtle); --sidebar-border: #456D55;
--sidebar-text: var(--krona-text-primary); --sidebar-text: #E8E5D9;
--sidebar-text-secondary: var(--krona-text-secondary); --sidebar-text-secondary: #BCC4B8;
--sidebar-text-muted: var(--krona-text-muted); --sidebar-text-muted: #9EAA9F;
--sidebar-text-active: var(--krona-primary); --sidebar-text-active: #FFFFFF;
--sidebar-icon: var(--krona-text-secondary); --sidebar-icon: #D0D4C8;
--sidebar-icon-active: var(--krona-primary); --sidebar-icon-active: #F0D39A;
--sidebar-indicator: var(--krona-gold); --sidebar-indicator: var(--krona-gold);
--text-primary: var(--krona-text-primary); --text-primary: var(--krona-text-primary);
@ -113,18 +114,18 @@
--surface-selected: var(--krona-surface-selected); --surface-selected: var(--krona-surface-selected);
--surface-disabled: var(--krona-surface-muted); --surface-disabled: var(--krona-surface-muted);
--sidebar-bg: var(--krona-surface); --sidebar-bg: #E9DDC5;
--sidebar-deep: var(--krona-bg-app); --sidebar-deep: #E3D4B8;
--sidebar-surface: var(--krona-surface-raised); --sidebar-surface: #F1E6D2;
--sidebar-hover: var(--krona-surface-hover); --sidebar-hover: #E2D4BA;
--sidebar-active: var(--krona-surface-selected); --sidebar-active: #DDC9A8;
--sidebar-border: var(--krona-border-subtle); --sidebar-border: #D4C09E;
--sidebar-text: var(--krona-text-primary); --sidebar-text: #51483D;
--sidebar-text-secondary: var(--krona-text-secondary); --sidebar-text-secondary: #756A5B;
--sidebar-text-muted: var(--krona-text-muted); --sidebar-text-muted: #8E8272;
--sidebar-text-active: var(--krona-primary); --sidebar-text-active: #684923;
--sidebar-icon: var(--krona-text-secondary); --sidebar-icon: #6F6456;
--sidebar-icon-active: var(--krona-primary); --sidebar-icon-active: #9A743D;
--sidebar-indicator: var(--krona-gold); --sidebar-indicator: var(--krona-gold);
--text-primary: var(--krona-text-primary); --text-primary: var(--krona-text-primary);
@ -202,17 +203,17 @@
--surface-selected: var(--krona-surface-selected); --surface-selected: var(--krona-surface-selected);
--surface-disabled: var(--krona-surface-muted); --surface-disabled: var(--krona-surface-muted);
--sidebar-bg: var(--krona-surface); --sidebar-bg: #18201B;
--sidebar-deep: var(--krona-bg-app); --sidebar-deep: #141916;
--sidebar-surface: var(--krona-surface-raised); --sidebar-surface: #202A23;
--sidebar-hover: var(--krona-surface-hover); --sidebar-hover: #27342B;
--sidebar-active: var(--krona-surface-selected); --sidebar-active: #2D3C31;
--sidebar-border: var(--krona-border-subtle); --sidebar-border: #354239;
--sidebar-text: var(--krona-text-primary); --sidebar-text: #E4E3DA;
--sidebar-text-secondary: var(--krona-text-secondary); --sidebar-text-secondary: #A9B0A8;
--sidebar-text-muted: var(--krona-text-muted); --sidebar-text-muted: #858E87;
--sidebar-text-active: var(--krona-primary); --sidebar-text-active: #F0E7D7;
--sidebar-icon: var(--krona-text-secondary); --sidebar-icon: #BEC4BB;
--sidebar-icon-active: var(--krona-gold); --sidebar-icon-active: var(--krona-gold);
--sidebar-indicator: var(--krona-gold); --sidebar-indicator: var(--krona-gold);

View file

@ -20,3 +20,11 @@ if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) {
Element.prototype.scrollIntoView = () => {}; Element.prototype.scrollIntoView = () => {};
} }
if (typeof window !== 'undefined' && !(window as any).ResizeObserver) {
(window as any).ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
}