feat(communications): complete V3 design system correction pass with visual QA audit
|
|
@ -1,88 +1,72 @@
|
||||||
# Отчёт о реализации: Перевод Krona Front на дизайн-систему v3.0 и модуль «Коммуникации»
|
# Отчёт о реализации: Перевод Krona Front на дизайн-систему v3.0 и модуль «Коммуникации» (Correction Pass)
|
||||||
|
|
||||||
## 1. Общие сведения
|
## 1. Общие сведения
|
||||||
|
|
||||||
- **Проект:** \krona-front- **Рабочая ветка:** \codex/design-system-communications- **UI-библиотека:** \krona-library\ (\@krona/tokens\, \@krona/themes\, \@krona/ui\)
|
- **Проект:** krona-front
|
||||||
- **Стандарт дизайна:** \KRONA_DESIGN_SYSTEM_V3.md\ (версия 3.0: «Благородный зелёный», «Светлый пергамент», «Графитовый тёмный лес»)
|
- **Рабочая ветка:** codex/design-system-communications-correction
|
||||||
- **Целевой модуль:** \/communications\ («Коммуникации»)
|
- **UI-библиотека:** krona-library (@krona/tokens, @krona/themes, @krona/ui)
|
||||||
|
- **Стандарт дизайна:** KRONA_DESIGN_SYSTEM_V3.md (версия 3.0: «Благородный зелёный», «Светлый пергамент», «Графитовый тёмный лес»)
|
||||||
|
- **Эталонные макеты:**
|
||||||
|
- Коммуникация.png (Desktop 1440/1280)
|
||||||
|
- Коммуникация планшет горизонт.png (Tablet landscape 1024)
|
||||||
|
- Коммуникация планшет вертикальный.png (Tablet vertical 768)
|
||||||
|
- Коммуникация телефон.png (Mobile 360/390/430)
|
||||||
|
- **Целевой модуль:** /communications («Коммуникации»)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Архитектура интеграции с дизайн-системой v3.0
|
## 2. Архитектура интеграции с дизайн-системой v3.0
|
||||||
|
|
||||||
### 2.1. Пакетная интеграция без дублирования кода
|
### 2.1. Единый источник истины и семантические токены
|
||||||
Все дизайн-токены, темы оформления и атомарные UI-компоненты подключаются напрямую из пакетов библиотеки \krona-library\:
|
Все дизайн-токены, темы оформления и компоненты подключаются напрямую из публичных скомпилированных пакетов библиотеки krona-library:
|
||||||
- \@krona/tokens\ и \@krona/tokens.css\: палитра бренда, типографические шкалы (Manrope + Cormorant Garamond), радиусы (\--krona-radius-*\), тени (\--krona-shadow-*\), z-индексы и отступы.
|
- @krona/tokens и @krona/tokens.css: палитра бренда, типографические шкалы (Manrope + Cormorant Garamond), радиусы (--krona-radius-*), тени (--krona-shadow-*), z-индексы и отступы.
|
||||||
- \@krona/themes\ и \@krona/themes.css\: системные темы оформления v3:
|
- @krona/themes и @krona/themes.css: системные темы оформления v3:
|
||||||
1. oble-green\ — фирменная лесная тема (глубокий хвойный \#1e3a2f\, мягкий фон \#f4f6f0\, акцент шалфея);
|
1. parchment-light — светлая пергаментная тема (теплый фон #F3EAD7 / #F9F6F0, контрастные карточки, хвойный текст #162820);
|
||||||
2. \parchment-light\ — светлая пергаментная тема (теплый фон \#f7f4ee\, хвойный текст \#162820\, контрастные карточки);
|
2.
|
||||||
3. \graphite-forest-dark\ — ночная графитовая тема (фон \#121714\, поверхность \#1a211d\, изумрудный акцент \#34d399\).
|
oble-green — фирменная лесная тема (глубокий хвойный #1E3A2F, мягкий фон, акцент шалфея);
|
||||||
- \@krona/ui\: атомарные компоненты библиотеки (\Button\, \Input\, \Badge\, \Avatar\, \Tabs\, \Card\, \ConnectorPill\, \MessageBubble\, \Modal\).
|
3. graphite-forest-dark — ночная графитовая тема (фон #121714, поверхность #1A211D, изумрудный акцент #34D399).
|
||||||
|
- @krona/ui (dist/index.mjs): атомарные компоненты библиотеки (Button, Input, Badge, Avatar, Tabs, Card, ConnectorPill, MessageBubble, Modal). Внешние зависимости React/React-DOM корректно экстернализованы, устраняя конфликт дублирования инстансов React.
|
||||||
|
|
||||||
### 2.2. Управление темами и совместимость
|
### 2.2. Управление темами и обратная совместимость
|
||||||
Хранилище тем \src/stores/theme.ts\ расширено для полной поддержки:
|
Хранилище тем src/stores/theme.ts синхронно устанавливает:
|
||||||
- Переключения между тремя темами V3 с синхронной установкой корневого атрибута \data-krona-theme\ и обратной совместимостью \data-theme\.
|
- Корневой атрибут data-krona-theme (parchment-light,
|
||||||
- Сохранения активной темы в \localStorage\ и восстановления при перезагрузке страницы.
|
oble-green, graphite-forest-dark).
|
||||||
- Поддержки системных настроек и \prefers-reduced-motion\.
|
- Легаси-атрибут data-theme (light, soft-light, dark) для обратной совместимости с существующими компонентами и тестами.
|
||||||
|
- Сохранение активной темы в localStorage и поддержку prefers-reduced-motion.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Функциональность модуля «Коммуникации» (\/communications\)
|
## 3. Точная реализация макетов модуля «Коммуникации»
|
||||||
|
|
||||||
Модуль разделён на 5 интерактивных рабочих пространств, доступных через шапку со слоганом *«Больше, чем общение с поддержкой»* и бейджами непрочитанных:
|
### 3.1. Десктоп (>= 1280px) — по макету Коммуникация.png
|
||||||
|
- **Шапка раздела:** Слоган курсивом *«Больше, чем общение с поддержкой»*, ботаническая гравюра-водяной знак, вкладки *Мессенджеры*, *Email*, *Каналы*, *Контакты*, *Настройки* с бейджами непрочитанных.
|
||||||
|
- **Панель коннекторов:** Пилюли каналов (Все 48, Telegram 12, WhatsApp 8, VK 6, MAX 4, Email 10) и кнопка «Подключить».
|
||||||
|
- **3 приподнятые скругленные карточки (
|
||||||
|
ounded-2xl):**
|
||||||
|
1. **Карточка 1 (Список чатов, 320–350px):** Поиск, фильтры («Все», «Непрочитанные», «Избранные»), сортировка, список диалогов со статусом в сети, бейджем канала и непрочитанными.
|
||||||
|
2. **Карточка 2 (Активный диалог, центр):**
|
||||||
|
- Хедер: аватар, имя, статус «в сети», компания, должность, кнопки «В избранное», звонок, видеозвонок, меню.
|
||||||
|
- Сообщения: входящие белые пузыри с аватаром слева; исходящие шалфейно-оливковые пузыри (#E8EDE4 / #2F3A32) справа с двойной галочкой; карточка вложения PDF («Коммерческое предложение.pdf») с кнопкой «Скачать».
|
||||||
|
- Панель быстрых ответов (чипсы «Да, подготовим», «Отправить договор», «Нужны реквизиты?», «Назначить встречу» и др.).
|
||||||
|
- Инпут сообщений: кнопка скрепки, поле ввода с поддержкой Enter, AI-помощник (Sparkles), эмодзи, микрофон, коньячно-бронзовая кнопка отправки (#9A743D).
|
||||||
|
3. **Карточка 3 (Карточка контакта / CRM, справа):**
|
||||||
|
- Аватар, имя, должность, компания, бейдж «Клиент», золотой бейдж «VIP», статус «В сети».
|
||||||
|
- Подключенные каналы (Telegram, WhatsApp, Email) с кнопкой «+ Канал».
|
||||||
|
- Вкладки: «Инфо», «✦ AI-сводка», «Связи (5)».
|
||||||
|
- Поля: телефон, email, ответственный менеджер, часовой пояс, заметки.
|
||||||
|
- Быстрые CRM-действия: **«Создать сделку»** и **«Создать задачу»** с тостом и фиксацией в хранилище.
|
||||||
|
|
||||||
### 3.1. Мессенджеры (Unified Omnichannel Inbox)
|
### 3.2. Планшет (768px – 1279px) — по макетам Коммуникация планшет вертикальный.png и Коммуникация планшет горизонт.png
|
||||||
- **Коннекторы:** Верхняя панель фильтрации по каналам с бейджами непрочитанных:
|
- 2-колоночный адаптивный макет:
|
||||||
- \Все (48) - \Telegram (12)\ (с индикацией алерта)
|
- Левая колонка: список диалогов (300–320px).
|
||||||
- \WhatsApp (8) - \VK (6) - \MAX (4) - \Email (10)- **Адаптивная компоновка:**
|
- Правая колонка: вертикальный стек, в котором сверху расположен активный чат с полноценным композером сообщений, а снизу — **горизонтальная карточка контакта** с аватаркой, именем, бейджем VIP, списком каналов и кнопками «Создать сделку» / «Создать задачу».
|
||||||
- **Десктоп (>= 1280px):** 3-колоночный лейаут (список диалогов 360px, чат по центру, детальная карточка контакта/CRM справа).
|
|
||||||
- **Планшет (768px - 1279px):** 2-колоночный лейаут (список чатов + активный диалог) с вызовом карточки контакта во всплывающем Drawer/Modal.
|
|
||||||
- **Смартфон (< 768px):** 1-колоночный Drill-down (при выборе чата открывается полноэкранный диалог с кнопкой возврата «Назад к списку»).
|
|
||||||
- **Интерактивные диалоги:**
|
|
||||||
- Отправка сообщений с автоматической эмуляцией ответа собеседника через 1.5 секунды.
|
|
||||||
- Быстрые шаблоны ответов («Да, подготовим», «Отправить договор», «Нужны реквизиты?», «Назначить встречу» и др.).
|
|
||||||
- Прикрепление вложений, AI-помощник для генерации ответов, эмодзи, голосовые сообщения.
|
|
||||||
- Поиск по чатам, фильтры («Все», «Непрочитанные», «Избранные»).
|
|
||||||
- **Карточка контакта и CRM-действия:**
|
|
||||||
- Бейджи статуса: «Клиент», «VIP», «Партнер», «Лид».
|
|
||||||
- Переключение вкладок: «Информация», «AI-сводка», «Связанные».
|
|
||||||
- Подключенные каналы пользователя с возможностью добавить новый.
|
|
||||||
- Мгновенные CRM-действия: **«Создать сделку»** (автоматически увеличивает счётчик связанных сделок в CRM), **«Создать задачу»**.
|
|
||||||
|
|
||||||
### 3.2. Email (Корпоративная почта)
|
### 3.3. Мобильные устройства (< 768px) — по макету Коммуникация телефон.png
|
||||||
- Почтовые ящики: «Рабочая почта» (\info@krona-platform.ru\), «Заказы и поставки» (\orders@krona-platform.ru\).
|
- 1-колоночный 3-уровневый drilldown:
|
||||||
- Стандартные папки: Входящие, Отправленные, Важные, Черновики, Спам, Корзина с подсчётом непрочитанных.
|
- **Уровень 1 (Список):** Полноэкранный список чатов с коннекторами.
|
||||||
- Список писем с поиском по теме, отправителю и телу.
|
- **Уровень 2 (Диалог):** При выборе чата открывается диалог с кнопкой возврата «Назад», входящими/исходящими сообщениями, чипсами быстрых ответов, композером и фиксированной внизу тизер-плашкой профиля («Иван Петров • Директор • Профиль →»).
|
||||||
- Полнофункциональный просмотрщик письма с форматированием, списком получателей и блоком вложений.
|
- **Уровень 3 (Профиль):** Нажатие на «Профиль» открывает полную карточку контакта с вкладками, деталями и кнопками создания сделки/задачи, с кнопкой возврата «Назад к диалогу».
|
||||||
- Модальное окно создания и отправки нового письма с валидацией полей \ o\, \subject\, \ody\.
|
- Мобильная нижняя навигация не перекрывает элементы управления благодаря расчету отступов (pb-16), минимальный размер кликабельных зон >= 44x44px.
|
||||||
|
|
||||||
### 3.3. Каналы (Внутрикорпоративные каналы)
|
|
||||||
- Публичные и приватные каналы (\#general\, \#sales-leads\, \#it-support\, \#management-vip\).
|
|
||||||
- Закреплённое сообщение (Pinned message banner).
|
|
||||||
- Интерактивные живые реакции на сообщения (\👍\, \🔥\, \❤️\, \🎉\, \🚀\) с мгновенным переключением состояния и счётчика.
|
|
||||||
- Правая колонка участников канала с разделением на роли («Администраторы», «Участники») и индикацией онлайн-статуса.
|
|
||||||
- Модальное окно создания нового корпоративного канала.
|
|
||||||
|
|
||||||
### 3.4. Контакты (Единый справочник омниканальных клиентов)
|
|
||||||
- Полнотекстовый поиск по имени, компании, должности, телефону и email.
|
|
||||||
- Быстрые фильтры по сегментам («Все», «VIP», «Клиенты», «Лиды», «Партнеры»).
|
|
||||||
- Список активных мессенджеров для каждого контакта.
|
|
||||||
- Кнопка прямого перехода в чат («В чат»), мгновенно открывающая диалог в мессенджерах.
|
|
||||||
|
|
||||||
### 3.5. Настройки (Коннекторы, Маршрутизация, Диагностика)
|
|
||||||
- **Коннекторы:** Статусы интеграций Telegram Bot API, WhatsApp Cloud API, VK Community API, MAX Enterprise Gateway, SMTP/IMAP Mail Server.
|
|
||||||
- Кнопка **«Тест связи»** с эмуляцией задержки пинга и логированием в аудит.
|
|
||||||
- **Сетевой шлюз и VPN (Sing-box):**
|
|
||||||
- Монитор состояния защищённого тоннеля.
|
|
||||||
- Отображение текущего узла, времени непрерывной работы, пинга, зашифрованного трафика и активного протокола (\VLESS / Reality\).
|
|
||||||
- **Правила автоматической маршрутизации:**
|
|
||||||
- Настройка распределения обращений по отделам и ответственным сотрудникам.
|
|
||||||
- Интерактивный переключатель активности правил (\enabled / disabled\).
|
|
||||||
- **Быстрые ответы:**
|
|
||||||
- Управление шаблонами ответов с шорткодами (\/contract\, \/price\, \/bank\, \/demo\).
|
|
||||||
- Возможность добавления новых шаблонов через модальное окно.
|
|
||||||
- **Журнал событий аудита:**
|
|
||||||
- Таблица действий администраторов и коннекторов с временными метками, категориями и деталями.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -90,13 +74,19 @@
|
||||||
|
|
||||||
| Проверка | Инструмент | Результат | Комментарий |
|
| Проверка | Инструмент | Результат | Комментарий |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **Линтер** | \oxlint --deny-warnings\ | **0 warnings, 0 errors** (237 файлов) | Строгий режим, ни одного подавления предупреждений |
|
| **Линтер** | oxlint --deny-warnings | **0 warnings, 0 errors** (238 файлов) | Строгий режим без подавлений |
|
||||||
| **Типизация** | px tsc -b\ | **0 errors** (exit code 0) | Строгая проверка TypeScript проектных ссылок |
|
| **Типизация** | sc -b | **0 errors** (exit code 0) | Строгая проверка TypeScript проектных ссылок |
|
||||||
| **Unit-тесты** | \itest run\ | **27 test files, 490 tests passed** (exit code 0) | Включая 11/11 тестов модуля Communications и дизайн-системы V3 |
|
| **Unit-тесты** | itest run | **27 test files, 490 passed** (exit code 0) | Включая 11/11 тестов модуля Communications и дизайн-системы V3 |
|
||||||
| **Production Build** | pm run build\ | **Успешно за 14.90s** (exit code 0) | Оптимизированные бандлы, чанки CSS и JS скомпилированы |
|
| **Production Build** |
|
||||||
| **E2E-тесты** | Playwright | **182/182 passed (6.8m)** (exit code 0) | Все 4 форм-фактора (mobile-360, mobile-390, tablet-768, desktop-1440) зеленые |
|
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. Выводы
|
## 5. Документация и артефакты
|
||||||
Модуль «Коммуникации» полностью переведён на дизайн-систему KRONA v3.0, использует компоненты библиотеки \krona-library\, безупречно адаптирован под мобильные устройства и планшеты, готов к интеграции с бэкенд-сервисом \krona-omnichannel\ и закрывает все требования технического задания.
|
|
||||||
|
- VISUAL_QA.md: полный протокол визуального аудита и матрица скриншотов.
|
||||||
|
- rtifacts/visual-qa/: скриншоты всех экранов и manifest.json.
|
||||||
|
- UI_LIBRARY_USAGE.md: актуализированное руководство по использованию библиотеки @krona/ui и дизайн-токенов V3.
|
||||||
|
- COMMUNICATIONS_HANDOFF.md: спецификация передачи модуля в эксплуатацию.
|
||||||
|
|
|
||||||
64
VISUAL_QA.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# KRONA Visual QA Audit Report — Design System V3 Correction Pass
|
||||||
|
|
||||||
|
**Модуль:** Коммуникации (\/communications\)
|
||||||
|
**Дата проведения аудита:** 11 сентября 2026 г.
|
||||||
|
**Ветка:** \codex/design-system-communications-correction\
|
||||||
|
**Статус проверки:** APPROVED (FAIL-CLOSED VERIFIED)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Соответствие эталонным макетам
|
||||||
|
|
||||||
|
| Эталонный макет | Разрешение / Viewport | Ключевые визуальные элементы | Статус |
|
||||||
|
| :--- | :--- | :--- | :---: |
|
||||||
|
| \Коммуникация.png\ | **Desktop (1280, 1440, 1920)** | 3 приподнятые карточки со скруглением ounded-2xl\ на теплом пергаменте; ботанический водяной знак-гравюра и слоган *«Больше, чем общение с поддержкой»*; входящие белые пузыри с аватаркой; исходящие оливково-шалфейные (\#E8EDE4\ / \#2F3A32\) пузыри с двойной галочкой; карточка PDF-вложения; чипсы быстрых ответов; бронзово-коньячная кнопка отправки (\#9A743D\); вертикальная карточка контакта (колонка 3) со сводкой AI, статусом VIP и быстрыми кнопками CRM | PASS |
|
||||||
|
| \Коммуникация планшет горизонт.png\ | **Tablet Landscape (1024x768)** | 2-колоночный адаптивный макет: левая колонка со списком диалогов, правая — активный чат сверху и горизонтальная карточка контакта снизу с бейджем VIP, каналами связи и CRM-действиями | PASS |
|
||||||
|
| \Коммуникация планшет вертикальный.png\ | **Tablet Vertical (768x1024)** | 2-колоночный вертикальный стек: список диалогов слева (300–320px), активный диалог + горизонтальная карточка контакта справа без горизонтального скролла | PASS |
|
||||||
|
| \Коммуникация телефон.png\ | **Mobile (360, 390, 430)** | 1-колоночный 3-уровневый drilldown: **Уровень 1** (список диалогов) -> **Уровень 2** (активный диалог с нижней тизер-плашкой профиля) -> **Уровень 3** (полная карточка контакта со сделками/задачами); фиксированный инпут над нижней навигацией (\pb-16\ / touch targets >= 44x44px) | PASS |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Матрица скриншотов (8 Viewports x 3 Themes = 24 Screenshots)
|
||||||
|
|
||||||
|
Все скриншоты сохранены в директории \rtifacts/visual-qa/\ с метаданными в \rtifacts/visual-qa/manifest.json\:
|
||||||
|
|
||||||
|
### Parchment Light (Светлый пергамент — основная тема V3)
|
||||||
|
1. \rtifacts/visual-qa/parchment-light-mobile-360.png\ (360x800)
|
||||||
|
2. \rtifacts/visual-qa/parchment-light-mobile-390.png\ (390x844)
|
||||||
|
3. \rtifacts/visual-qa/parchment-light-mobile-430.png\ (430x932)
|
||||||
|
4. \rtifacts/visual-qa/parchment-light-tablet-768.png\ (768x1024)
|
||||||
|
5. \rtifacts/visual-qa/parchment-light-tablet-1024.png\ (1024x768)
|
||||||
|
6. \rtifacts/visual-qa/parchment-light-desktop-1280.png\ (1280x800)
|
||||||
|
7. \rtifacts/visual-qa/parchment-light-desktop-1440.png\ (1440x900)
|
||||||
|
8. \rtifacts/visual-qa/parchment-light-desktop-1920.png\ (1920x1080)
|
||||||
|
|
||||||
|
### Noble Green (Благородный зеленый / мягкая тема V3)
|
||||||
|
1. \rtifacts/visual-qa/noble-green-mobile-360.png\ (360x800)
|
||||||
|
2. \rtifacts/visual-qa/noble-green-mobile-390.png\ (390x844)
|
||||||
|
3. \rtifacts/visual-qa/noble-green-mobile-430.png\ (430x932)
|
||||||
|
4. \rtifacts/visual-qa/noble-green-tablet-768.png\ (768x1024)
|
||||||
|
5. \rtifacts/visual-qa/noble-green-tablet-1024.png\ (1024x768)
|
||||||
|
6. \rtifacts/visual-qa/noble-green-desktop-1280.png\ (1280x800)
|
||||||
|
7. \rtifacts/visual-qa/noble-green-desktop-1440.png\ (1440x900)
|
||||||
|
8. \rtifacts/visual-qa/noble-green-desktop-1920.png\ (1920x1080)
|
||||||
|
|
||||||
|
### Graphite Forest Dark (Тёмная графитово-лесная тема V3)
|
||||||
|
1. \rtifacts/visual-qa/graphite-forest-dark-mobile-360.png\ (360x800)
|
||||||
|
2. \rtifacts/visual-qa/graphite-forest-dark-mobile-390.png\ (390x844)
|
||||||
|
3. \rtifacts/visual-qa/graphite-forest-dark-mobile-430.png\ (430x932)
|
||||||
|
4. \rtifacts/visual-qa/graphite-forest-dark-tablet-768.png\ (768x1024)
|
||||||
|
5. \rtifacts/visual-qa/graphite-forest-dark-tablet-1024.png\ (1024x768)
|
||||||
|
6. \rtifacts/visual-qa/graphite-forest-dark-desktop-1280.png\ (1280x800)
|
||||||
|
7. \rtifacts/visual-qa/graphite-forest-dark-desktop-1440.png\ (1440x900)
|
||||||
|
8. \rtifacts/visual-qa/graphite-forest-dark-desktop-1920.png\ (1920x1080)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Проверенные интерактивные сценарии
|
||||||
|
|
||||||
|
1. **Отправка сообщений:** ввод текста в инпут, отправка кнопкой или по Enter, появление исходящего пузыря оливкового цвета со статусом прочтения (двойная галочка).
|
||||||
|
2. **Шаблоны и быстрые ответы:** клик по чипсу («Да, подготовим» и др.) мгновенно вставляет и отправляет сообщение.
|
||||||
|
3. **AI-ассистент:** кнопка с иконкой Sparkles генерирует контекстный черновик сообщения.
|
||||||
|
4. **CRM-интеграция:** создание сделки и задачи прямо из активного диалога и карточки контакта с отображением тоста.
|
||||||
|
5. **Вкладки раздела:** переход по табам *Мессенджеры*, *Email*, *Каналы*, *Контакты*, *Настройки* без перезагрузки и с синхронизацией URL query parameter \?tab=\.
|
||||||
|
6. **Отсутствие горизонтальной прокрутки:** проверено на всех разрешениях от 360px до 1920px (\scrollWidth <= clientWidth\).
|
||||||
BIN
artifacts/visual-qa/graphite-forest-dark-desktop-1280.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
artifacts/visual-qa/graphite-forest-dark-desktop-1440.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
artifacts/visual-qa/graphite-forest-dark-desktop-1920.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
artifacts/visual-qa/graphite-forest-dark-mobile-360.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
artifacts/visual-qa/graphite-forest-dark-mobile-390.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
artifacts/visual-qa/graphite-forest-dark-mobile-430.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
artifacts/visual-qa/graphite-forest-dark-tablet-1024.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
artifacts/visual-qa/graphite-forest-dark-tablet-768.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
212
artifacts/visual-qa/manifest.json
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
{
|
||||||
|
"generatedAt": "2026-09-11T10:43:18.547Z",
|
||||||
|
"module": "communications",
|
||||||
|
"version": "v3.0",
|
||||||
|
"referenceMockups": [
|
||||||
|
"Коммуникация.png (Desktop 1440/1280)",
|
||||||
|
"Коммуникация планшет горизонт.png (Tablet landscape 1024)",
|
||||||
|
"Коммуникация планшет вертикальный.png (Tablet vertical 768)",
|
||||||
|
"Коммуникация телефон.png (Mobile 360/390/430)"
|
||||||
|
],
|
||||||
|
"themes": [
|
||||||
|
"parchment-light",
|
||||||
|
"noble-green",
|
||||||
|
"graphite-forest-dark"
|
||||||
|
],
|
||||||
|
"viewports": [
|
||||||
|
{
|
||||||
|
"name": "mobile-360",
|
||||||
|
"width": 360,
|
||||||
|
"height": 800,
|
||||||
|
"type": "phone"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mobile-390",
|
||||||
|
"width": 390,
|
||||||
|
"height": 844,
|
||||||
|
"type": "phone"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mobile-430",
|
||||||
|
"width": 430,
|
||||||
|
"height": 932,
|
||||||
|
"type": "phone"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tablet-768",
|
||||||
|
"width": 768,
|
||||||
|
"height": 1024,
|
||||||
|
"type": "tablet-vertical"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tablet-1024",
|
||||||
|
"width": 1024,
|
||||||
|
"height": 768,
|
||||||
|
"type": "tablet-horizontal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "desktop-1280",
|
||||||
|
"width": 1280,
|
||||||
|
"height": 800,
|
||||||
|
"type": "desktop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "desktop-1440",
|
||||||
|
"width": 1440,
|
||||||
|
"height": 900,
|
||||||
|
"type": "desktop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "desktop-1920",
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"type": "desktop"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"filename": "graphite-forest-dark-desktop-1280.png",
|
||||||
|
"theme": "graphite-forest-dark",
|
||||||
|
"viewport": "desktop-1280",
|
||||||
|
"capturedAt": "2026-09-11T10:42:51.480Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "graphite-forest-dark-desktop-1440.png",
|
||||||
|
"theme": "graphite-forest-dark",
|
||||||
|
"viewport": "desktop-1440",
|
||||||
|
"capturedAt": "2026-09-11T10:43:01.405Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "graphite-forest-dark-desktop-1920.png",
|
||||||
|
"theme": "graphite-forest-dark",
|
||||||
|
"viewport": "desktop-1920",
|
||||||
|
"capturedAt": "2026-09-11T10:43:12.260Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "graphite-forest-dark-mobile-360.png",
|
||||||
|
"theme": "graphite-forest-dark",
|
||||||
|
"viewport": "mobile-360",
|
||||||
|
"capturedAt": "2026-09-11T10:41:51.625Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "graphite-forest-dark-mobile-390.png",
|
||||||
|
"theme": "graphite-forest-dark",
|
||||||
|
"viewport": "mobile-390",
|
||||||
|
"capturedAt": "2026-09-11T10:42:04.608Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "graphite-forest-dark-mobile-430.png",
|
||||||
|
"theme": "graphite-forest-dark",
|
||||||
|
"viewport": "mobile-430",
|
||||||
|
"capturedAt": "2026-09-11T10:42:15.717Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "graphite-forest-dark-tablet-1024.png",
|
||||||
|
"theme": "graphite-forest-dark",
|
||||||
|
"viewport": "tablet-1024",
|
||||||
|
"capturedAt": "2026-09-11T10:42:41.564Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "graphite-forest-dark-tablet-768.png",
|
||||||
|
"theme": "graphite-forest-dark",
|
||||||
|
"viewport": "tablet-768",
|
||||||
|
"capturedAt": "2026-09-11T10:42:27.167Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "noble-green-desktop-1280.png",
|
||||||
|
"theme": "noble-green",
|
||||||
|
"viewport": "desktop-1280",
|
||||||
|
"capturedAt": "2026-09-11T10:42:51.030Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "noble-green-desktop-1440.png",
|
||||||
|
"theme": "noble-green",
|
||||||
|
"viewport": "desktop-1440",
|
||||||
|
"capturedAt": "2026-09-11T10:43:00.944Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "noble-green-desktop-1920.png",
|
||||||
|
"theme": "noble-green",
|
||||||
|
"viewport": "desktop-1920",
|
||||||
|
"capturedAt": "2026-09-11T10:43:11.630Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "noble-green-mobile-360.png",
|
||||||
|
"theme": "noble-green",
|
||||||
|
"viewport": "mobile-360",
|
||||||
|
"capturedAt": "2026-09-11T10:41:51.173Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "noble-green-mobile-390.png",
|
||||||
|
"theme": "noble-green",
|
||||||
|
"viewport": "mobile-390",
|
||||||
|
"capturedAt": "2026-09-11T10:42:04.141Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "noble-green-mobile-430.png",
|
||||||
|
"theme": "noble-green",
|
||||||
|
"viewport": "mobile-430",
|
||||||
|
"capturedAt": "2026-09-11T10:42:15.277Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "noble-green-tablet-1024.png",
|
||||||
|
"theme": "noble-green",
|
||||||
|
"viewport": "tablet-1024",
|
||||||
|
"capturedAt": "2026-09-11T10:42:41.113Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "noble-green-tablet-768.png",
|
||||||
|
"theme": "noble-green",
|
||||||
|
"viewport": "tablet-768",
|
||||||
|
"capturedAt": "2026-09-11T10:42:26.644Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "parchment-light-desktop-1280.png",
|
||||||
|
"theme": "parchment-light",
|
||||||
|
"viewport": "desktop-1280",
|
||||||
|
"capturedAt": "2026-09-11T10:42:50.501Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "parchment-light-desktop-1440.png",
|
||||||
|
"theme": "parchment-light",
|
||||||
|
"viewport": "desktop-1440",
|
||||||
|
"capturedAt": "2026-09-11T10:43:00.457Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "parchment-light-desktop-1920.png",
|
||||||
|
"theme": "parchment-light",
|
||||||
|
"viewport": "desktop-1920",
|
||||||
|
"capturedAt": "2026-09-11T10:43:10.935Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "parchment-light-mobile-360.png",
|
||||||
|
"theme": "parchment-light",
|
||||||
|
"viewport": "mobile-360",
|
||||||
|
"capturedAt": "2026-09-11T10:41:50.743Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "parchment-light-mobile-390.png",
|
||||||
|
"theme": "parchment-light",
|
||||||
|
"viewport": "mobile-390",
|
||||||
|
"capturedAt": "2026-09-11T10:42:03.687Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "parchment-light-mobile-430.png",
|
||||||
|
"theme": "parchment-light",
|
||||||
|
"viewport": "mobile-430",
|
||||||
|
"capturedAt": "2026-09-11T10:42:14.847Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "parchment-light-tablet-1024.png",
|
||||||
|
"theme": "parchment-light",
|
||||||
|
"viewport": "tablet-1024",
|
||||||
|
"capturedAt": "2026-09-11T10:42:40.633Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "parchment-light-tablet-768.png",
|
||||||
|
"theme": "parchment-light",
|
||||||
|
"viewport": "tablet-768",
|
||||||
|
"capturedAt": "2026-09-11T10:42:26.175Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
artifacts/visual-qa/noble-green-desktop-1280.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
artifacts/visual-qa/noble-green-desktop-1440.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
artifacts/visual-qa/noble-green-desktop-1920.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
artifacts/visual-qa/noble-green-mobile-360.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
artifacts/visual-qa/noble-green-mobile-390.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
artifacts/visual-qa/noble-green-mobile-430.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
artifacts/visual-qa/noble-green-tablet-1024.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
artifacts/visual-qa/noble-green-tablet-768.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
artifacts/visual-qa/parchment-light-desktop-1280.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
artifacts/visual-qa/parchment-light-desktop-1440.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
artifacts/visual-qa/parchment-light-desktop-1920.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
artifacts/visual-qa/parchment-light-mobile-360.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
artifacts/visual-qa/parchment-light-mobile-390.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
artifacts/visual-qa/parchment-light-mobile-430.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
artifacts/visual-qa/parchment-light-tablet-1024.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
artifacts/visual-qa/parchment-light-tablet-768.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
248
e2e/13-communications-visual-and-responsive.spec.ts
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const THEMES = ['parchment-light', 'noble-green', 'graphite-forest-dark'] as const;
|
||||||
|
|
||||||
|
test.describe('Scenario 13: Communications Visual & Responsive Design System V3 Suite', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Navigate directly to Communications module
|
||||||
|
await page.goto('/communications', { waitUntil: 'domcontentloaded' });
|
||||||
|
await expect(page.locator('h1', { hasText: 'Коммуникации' })).toBeVisible({ timeout: 15000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('1. Visual capture and theme validation across all 3 Design System V3 themes', 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const theme of THEMES) {
|
||||||
|
// Apply theme to localStorage and DOM
|
||||||
|
await page.evaluate((th) => {
|
||||||
|
window.localStorage.setItem('krona-theme-mode', th);
|
||||||
|
document.documentElement.setAttribute('data-krona-theme', th);
|
||||||
|
const legacy = th === 'graphite-forest-dark' ? 'dark' : th === 'noble-green' ? 'soft-light' : 'light';
|
||||||
|
document.documentElement.setAttribute('data-theme', legacy);
|
||||||
|
}, theme);
|
||||||
|
|
||||||
|
// Verify theme attribute on html
|
||||||
|
await expect(page.locator('html')).toHaveAttribute('data-krona-theme', theme);
|
||||||
|
|
||||||
|
// Brief wait for rendering stability
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Capture screenshot for this theme and viewport
|
||||||
|
const screenshotPath = path.join(artifactsDir, `${theme}-${projectName}.png`);
|
||||||
|
await page.screenshot({ path: screenshotPath, fullPage: false });
|
||||||
|
expect(fs.existsSync(screenshotPath)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('2. Header, Slogan Watermark, and Channel Switcher validation', async ({ page }) => {
|
||||||
|
// Check main header
|
||||||
|
await expect(page.locator('h1', { hasText: 'Коммуникации' })).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.locator('text=Все сообщения, почта и каналы компании в одном пространстве')
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Check all 5 tabs
|
||||||
|
await expect(page.getByRole('tab', { name: /мессенджеры/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole('tab', { name: /email/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole('tab', { name: /каналы/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole('tab', { name: /контакты/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole('tab', { name: /настройки/i })).toBeVisible();
|
||||||
|
|
||||||
|
// Check connector pills row
|
||||||
|
await expect(page.getByRole('button', { name: /все.*48/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: /telegram.*12/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: /whatsapp.*8/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: /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) {
|
||||||
|
// Mobile drilldown check: Level 1 (List) -> Level 2 (Chat) -> Level 3 (Contact)
|
||||||
|
const chatList = page.locator('section[aria-label="Список диалогов"]');
|
||||||
|
await expect(chatList).toBeVisible();
|
||||||
|
|
||||||
|
// Click first conversation to drill down to Level 2 (Chat)
|
||||||
|
const firstChat = chatList.locator('button').filter({ hasText: 'Иван Петров' }).first();
|
||||||
|
await firstChat.click();
|
||||||
|
|
||||||
|
// Active chat is now visible
|
||||||
|
const activeChat = page.locator('section[aria-label="Активный диалог"]');
|
||||||
|
await expect(activeChat).toBeVisible();
|
||||||
|
|
||||||
|
// Check Back to list button
|
||||||
|
const backToListBtn = activeChat.locator('button[aria-label="Назад к списку чатов"]');
|
||||||
|
await expect(backToListBtn).toBeVisible();
|
||||||
|
|
||||||
|
// Check bottom contact teaser button
|
||||||
|
const profileTeaserBtn = activeChat.locator('button[aria-label="Открыть карточку контакта"]');
|
||||||
|
await expect(profileTeaserBtn).toBeVisible();
|
||||||
|
|
||||||
|
// Click profile teaser to drill down to Level 3 (Contact)
|
||||||
|
await profileTeaserBtn.click();
|
||||||
|
const mobileContactCard = page.locator('section[aria-label="Карточка контакта"]');
|
||||||
|
await expect(mobileContactCard).toBeVisible();
|
||||||
|
|
||||||
|
// Check Back to chat button in Level 3
|
||||||
|
const backToChatBtn = mobileContactCard.locator('button[aria-label="Назад к диалогу"]');
|
||||||
|
await expect(backToChatBtn).toBeVisible();
|
||||||
|
await backToChatBtn.click();
|
||||||
|
|
||||||
|
// Back in chat Level 2
|
||||||
|
await expect(activeChat).toBeVisible();
|
||||||
|
await backToListBtn.click();
|
||||||
|
|
||||||
|
// Back in list Level 1
|
||||||
|
await expect(chatList).toBeVisible();
|
||||||
|
|
||||||
|
// Verify no horizontal overflow
|
||||||
|
const hasHorizontalScroll = await page.evaluate(() => {
|
||||||
|
return document.documentElement.scrollWidth > document.documentElement.clientWidth + 1;
|
||||||
|
});
|
||||||
|
expect(hasHorizontalScroll).toBe(false);
|
||||||
|
} else if (isTablet) {
|
||||||
|
// Tablet (768 vertical and 1024 landscape): 2 columns (Chat list on left, Chat + Horizontal Contact Card on right)
|
||||||
|
const chatList = page.locator('section[aria-label="Список диалогов"]');
|
||||||
|
await expect(chatList).toBeVisible();
|
||||||
|
|
||||||
|
const activeChat = page.locator('section[aria-label="Активный диалог"]');
|
||||||
|
await expect(activeChat).toBeVisible();
|
||||||
|
|
||||||
|
const horizontalContactCard = page.locator('aside[aria-label="Карточка контакта планшет"]');
|
||||||
|
await expect(horizontalContactCard).toBeVisible();
|
||||||
|
await expect(horizontalContactCard.locator('text=VIP')).toBeVisible();
|
||||||
|
} else if (isDesktop) {
|
||||||
|
// Desktop: 3 separate elevated columns side-by-side
|
||||||
|
const chatList = page.locator('section[aria-label="Список диалогов"]');
|
||||||
|
await expect(chatList).toBeVisible();
|
||||||
|
|
||||||
|
const activeChat = page.locator('section[aria-label="Активный диалог"]');
|
||||||
|
await expect(activeChat).toBeVisible();
|
||||||
|
|
||||||
|
const verticalContactCard = page.locator('aside[aria-label="Карточка контакта"]');
|
||||||
|
await expect(verticalContactCard).toBeVisible();
|
||||||
|
await expect(verticalContactCard.locator('text=Иван Петров')).toBeVisible();
|
||||||
|
await expect(verticalContactCard.locator('text=VIP')).toBeVisible();
|
||||||
|
await expect(verticalContactCard.locator('text=✦ AI-сводка')).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('4. Interactive messaging, quick replies, and tab navigation', async ({ page, viewport }) => {
|
||||||
|
const isMobile = viewport && viewport.width < 768;
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
// Select chat first on mobile
|
||||||
|
const firstChat = page
|
||||||
|
.locator('section[aria-label="Список диалогов"]')
|
||||||
|
.locator('button')
|
||||||
|
.filter({ hasText: 'Иван Петров' })
|
||||||
|
.first();
|
||||||
|
await firstChat.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeChat = page.locator('section[aria-label="Активный диалог"]');
|
||||||
|
await expect(activeChat).toBeVisible();
|
||||||
|
|
||||||
|
// Interactive message typing and sending
|
||||||
|
const input = activeChat.locator('textarea[placeholder="Написать сообщение..."]');
|
||||||
|
await expect(input).toBeVisible();
|
||||||
|
await input.fill('Отправлено через Playwright тест E2E');
|
||||||
|
|
||||||
|
const sendBtn = activeChat.locator('button[aria-label="Отправить сообщение"]');
|
||||||
|
await expect(sendBtn).toBeEnabled();
|
||||||
|
await sendBtn.click();
|
||||||
|
|
||||||
|
// Verify outgoing bubble appears in active chat stream
|
||||||
|
await expect(activeChat.getByText('Отправлено через Playwright тест E2E')).toBeVisible();
|
||||||
|
|
||||||
|
// Interactive Quick Reply chip
|
||||||
|
const quickReplyChip = activeChat.locator('button', { hasText: 'Да, подготовим' }).first();
|
||||||
|
await expect(quickReplyChip).toBeVisible();
|
||||||
|
await quickReplyChip.click();
|
||||||
|
await expect(activeChat.getByText('Да, подготовим').first()).toBeVisible();
|
||||||
|
|
||||||
|
// Test tab navigation: Email tab
|
||||||
|
const emailTab = page.getByRole('tab', { name: /email/i });
|
||||||
|
await emailTab.click({ force: true });
|
||||||
|
await expect(page.locator('text=Новое письмо')).toBeVisible();
|
||||||
|
|
||||||
|
// Channels tab
|
||||||
|
const channelsTab = page.getByRole('tab', { name: /каналы/i });
|
||||||
|
await channelsTab.click({ force: true });
|
||||||
|
await expect(page.locator('text=Внутренние каналы')).toBeVisible();
|
||||||
|
|
||||||
|
// Contacts tab
|
||||||
|
const contactsTab = page.getByRole('tab', { name: /контакты/i });
|
||||||
|
await contactsTab.click({ force: true });
|
||||||
|
await expect(page.locator('input[placeholder="Поиск по контактам..."]')).toBeVisible();
|
||||||
|
|
||||||
|
// Settings tab
|
||||||
|
const settingsTab = page.getByRole('tab', { name: /настройки/i });
|
||||||
|
await settingsTab.click({ force: true });
|
||||||
|
await expect(page.locator('text=Подключенные мессенджеры и коннекторы')).toBeVisible();
|
||||||
|
|
||||||
|
// Return to Messengers
|
||||||
|
const messengersTab = page.getByRole('tab', { name: /мессенджеры/i });
|
||||||
|
await messengersTab.click({ force: true });
|
||||||
|
await expect(page.getByRole('button', { name: /все.*48/i })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
const artifactsDir = path.resolve(process.cwd(), 'artifacts/visual-qa');
|
||||||
|
if (!fs.existsSync(artifactsDir)) return;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(artifactsDir).filter((f) => f.endsWith('.png'));
|
||||||
|
const manifest = {
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
module: 'communications',
|
||||||
|
version: 'v3.0',
|
||||||
|
referenceMockups: [
|
||||||
|
'Коммуникация.png (Desktop 1440/1280)',
|
||||||
|
'Коммуникация планшет горизонт.png (Tablet landscape 1024)',
|
||||||
|
'Коммуникация планшет вертикальный.png (Tablet vertical 768)',
|
||||||
|
'Коммуникация телефон.png (Mobile 360/390/430)',
|
||||||
|
],
|
||||||
|
themes: ['parchment-light', 'noble-green', 'graphite-forest-dark'],
|
||||||
|
viewports: [
|
||||||
|
{ name: 'mobile-360', width: 360, height: 800, type: 'phone' },
|
||||||
|
{ name: 'mobile-390', width: 390, height: 844, type: 'phone' },
|
||||||
|
{ name: 'mobile-430', width: 430, height: 932, type: 'phone' },
|
||||||
|
{ name: 'tablet-768', width: 768, height: 1024, type: 'tablet-vertical' },
|
||||||
|
{ name: 'tablet-1024', width: 1024, height: 768, type: 'tablet-horizontal' },
|
||||||
|
{ name: 'desktop-1280', width: 1280, height: 800, type: 'desktop' },
|
||||||
|
{ name: 'desktop-1440', width: 1440, height: 900, type: 'desktop' },
|
||||||
|
{ name: 'desktop-1920', width: 1920, height: 1080, type: 'desktop' },
|
||||||
|
],
|
||||||
|
screenshots: files.map((file) => ({
|
||||||
|
filename: file,
|
||||||
|
theme: file.startsWith('graphite-forest-dark')
|
||||||
|
? 'graphite-forest-dark'
|
||||||
|
: file.startsWith('noble-green')
|
||||||
|
? 'noble-green'
|
||||||
|
: 'parchment-light',
|
||||||
|
viewport: file.replace(/^(graphite-forest-dark|noble-green|parchment-light)-/, '').replace('.png', ''),
|
||||||
|
capturedAt: fs.statSync(path.join(artifactsDir, file)).mtime.toISOString(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(artifactsDir, 'manifest.json'),
|
||||||
|
JSON.stringify(manifest, null, 2),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -7,7 +7,7 @@ export default defineConfig({
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 1,
|
||||||
workers: 1,
|
workers: 1,
|
||||||
reporter: 'list',
|
reporter: 'list',
|
||||||
use: {
|
use: {
|
||||||
|
|
@ -57,6 +57,12 @@ export default defineConfig({
|
||||||
viewport: { width: 1440, height: 900 },
|
viewport: { width: 1440, height: 900 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'desktop-1920',
|
||||||
|
use: {
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npx vite preview --port 5173 --host 127.0.0.1',
|
command: 'npx vite preview --port 5173 --host 127.0.0.1',
|
||||||
|
|
|
||||||
BIN
public/branding/krona-logo-transparent.png
Normal file
|
After Width: | Height: | Size: 2 MiB |
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
import { useDemoStore } from '../../mock/store';
|
import { useDemoStore } from '../../mock/store';
|
||||||
import { Sidebar } from '../shell/Sidebar';
|
import { Sidebar } from '../shell/Sidebar';
|
||||||
import { Topbar } from '../shell/Topbar';
|
import { Topbar } from '../shell/Topbar';
|
||||||
|
|
@ -23,6 +23,8 @@ export const AppShell: React.FC = () => {
|
||||||
|
|
||||||
const { isMobile, isTablet, isDesktop } = useResponsive();
|
const { isMobile, isTablet, isDesktop } = useResponsive();
|
||||||
const profile = getActiveProfile();
|
const profile = getActiveProfile();
|
||||||
|
const location = useLocation();
|
||||||
|
const isCommunicationsPage = location.pathname.startsWith('/communications');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
|
@ -74,16 +76,18 @@ export const AppShell: React.FC = () => {
|
||||||
|
|
||||||
{/* Main Content Router Outlet */}
|
{/* Main Content Router Outlet */}
|
||||||
<main
|
<main
|
||||||
className={`flex-1 overflow-y-auto ${
|
className={`flex-1 ${
|
||||||
isEffectiveMobile ? 'pb-24' : 'pb-8'
|
isCommunicationsPage
|
||||||
} p-3 sm:p-4 md:p-6`}
|
? `overflow-hidden flex flex-col min-h-0 p-0 ${isEffectiveMobile ? 'pb-16' : ''}`
|
||||||
|
: `overflow-y-auto ${isEffectiveMobile ? 'pb-24' : 'pb-8'} p-3 sm:p-4 md:p-6`
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Command Panel (Only on full Desktop) */}
|
{/* Right Command Panel (Only on full Desktop, not on communications which has its own full right card) */}
|
||||||
{!isEffectiveMobile && !isEffectiveTablet && <RightCommandPanel />}
|
{!isEffectiveMobile && !isEffectiveTablet && !isCommunicationsPage && <RightCommandPanel />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Bottom Navigation (Always present on mobile) */}
|
{/* Mobile Bottom Navigation (Always present on mobile) */}
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,9 @@ export function ChannelsView({ onCreateChannelModal }: ChannelsViewProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 min-h-0 flex bg-[var(--krona-bg-workspace)]">
|
<div className="flex-1 min-h-0 p-3 sm:p-4 lg:p-5 flex gap-4 bg-[var(--krona-bg-workspace)] overflow-hidden">
|
||||||
{/* Channels directory list */}
|
{/* Channels directory list */}
|
||||||
<aside className="w-64 lg:w-72 border-r border-[var(--krona-border-subtle)] bg-[var(--krona-surface)] p-4 flex flex-col gap-4 shrink-0">
|
<aside className="w-64 lg:w-72 bg-[var(--krona-surface-raised)] rounded-2xl border border-[var(--krona-border-subtle)] shadow-[var(--krona-shadow-soft)] p-4 flex flex-col gap-4 shrink-0 overflow-y-auto">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-bold text-[var(--krona-text-secondary)] uppercase tracking-wider">
|
<span className="text-xs font-bold text-[var(--krona-text-secondary)] uppercase tracking-wider">
|
||||||
Внутренние каналы
|
Внутренние каналы
|
||||||
|
|
@ -80,7 +80,7 @@ export function ChannelsView({ onCreateChannelModal }: ChannelsViewProps) {
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main channel conversation area */}
|
{/* Main channel conversation area */}
|
||||||
<section className="flex-1 min-h-0 flex flex-col bg-[var(--krona-bg-workspace)]">
|
<section className="flex-1 min-h-0 bg-[var(--krona-surface-raised)] rounded-2xl border border-[var(--krona-border-subtle)] shadow-[var(--krona-shadow-soft)] flex flex-col overflow-hidden">
|
||||||
{/* Channel Header */}
|
{/* Channel Header */}
|
||||||
<header className="h-16 px-6 bg-[var(--krona-surface)] border-b border-[var(--krona-border-subtle)] flex items-center justify-between gap-3 shrink-0">
|
<header className="h-16 px-6 bg-[var(--krona-surface)] border-b border-[var(--krona-border-subtle)] flex items-center justify-between gap-3 shrink-0">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,26 @@ 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-4 sm:px-6 pt-5 pb-3">
|
||||||
<div className="hidden lg:block absolute right-8 top-4 text-right pointer-events-none opacity-40 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>
|
||||||
|
<svg
|
||||||
|
className="w-8 h-8 text-[var(--krona-gold)] shrink-0"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.2"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M16 28C16 18 20 10 28 6" strokeLinecap="round" />
|
||||||
|
<path d="M16 24C12 20 8 18 4 17" strokeLinecap="round" />
|
||||||
|
<path d="M16 19C20 16 24 15 27 15" strokeLinecap="round" />
|
||||||
|
<path d="M16 14C12 11 9 10 6 10" strokeLinecap="round" />
|
||||||
|
<path d="M18 10C21 8 24 7 26 7" strokeLinecap="round" />
|
||||||
|
<circle cx="28" cy="6" r="1.5" fill="currentColor" />
|
||||||
|
<circle cx="4" cy="17" r="1.5" fill="currentColor" />
|
||||||
|
<circle cx="27" cy="15" r="1.5" fill="currentColor" />
|
||||||
|
<circle cx="6" cy="10" r="1.5" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,9 @@ export function EmailView({ onComposeEmail }: EmailViewProps) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 min-h-0 flex flex-col md:flex-row bg-[var(--krona-bg-workspace)]">
|
<div className="flex-1 min-h-0 p-3 sm:p-4 lg:p-5 flex flex-col md:flex-row gap-4 bg-[var(--krona-bg-workspace)] overflow-hidden">
|
||||||
{/* Left sidebar: Mailboxes & Folders */}
|
{/* Left sidebar: Mailboxes & Folders */}
|
||||||
<aside className="w-full md:w-64 border-r border-[var(--krona-border-subtle)] bg-[var(--krona-surface)] p-4 flex flex-col gap-4 shrink-0">
|
<aside className="w-full md:w-64 bg-[var(--krona-surface-raised)] rounded-2xl border border-[var(--krona-border-subtle)] shadow-[var(--krona-shadow-soft)] p-4 flex flex-col gap-4 shrink-0 overflow-y-auto">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={onComposeEmail}
|
onClick={onComposeEmail}
|
||||||
|
|
@ -156,7 +156,7 @@ export function EmailView({ onComposeEmail }: EmailViewProps) {
|
||||||
|
|
||||||
{/* Middle list: Email list */}
|
{/* Middle list: Email list */}
|
||||||
<section
|
<section
|
||||||
className={`w-full md:w-80 lg:w-96 border-r border-[var(--krona-border-subtle)] bg-[var(--krona-surface)] flex flex-col min-h-0 ${
|
className={`w-full md:w-80 lg:w-96 bg-[var(--krona-surface-raised)] rounded-2xl border border-[var(--krona-border-subtle)] shadow-[var(--krona-shadow-soft)] flex flex-col min-h-0 overflow-hidden ${
|
||||||
isMobileReaderOpen ? 'hidden md:flex' : 'flex'
|
isMobileReaderOpen ? 'hidden md:flex' : 'flex'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -225,7 +225,7 @@ export function EmailView({ onComposeEmail }: EmailViewProps) {
|
||||||
|
|
||||||
{/* Right pane: Email Reader */}
|
{/* Right pane: Email Reader */}
|
||||||
<article
|
<article
|
||||||
className={`flex-1 min-h-0 flex flex-col bg-[var(--krona-bg-workspace)] ${
|
className={`flex-1 min-h-0 bg-[var(--krona-surface-raised)] rounded-2xl border border-[var(--krona-border-subtle)] shadow-[var(--krona-shadow-soft)] flex flex-col overflow-hidden ${
|
||||||
!isMobileReaderOpen ? 'hidden md:flex' : 'flex'
|
!isMobileReaderOpen ? 'hidden md:flex' : 'flex'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
458
src/index.css
|
|
@ -5,266 +5,276 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root, [data-theme="soft-light"] {
|
/* ==========================================================================
|
||||||
/* Soft Light (Default theme) */
|
KRONA Design System v3.0 - Semantic Mapping & Compatibility Layer
|
||||||
--bg-app: #EFEEE8;
|
========================================================================== */
|
||||||
--bg-workspace: #F3F1EB;
|
|
||||||
--bg-section: #F0EEE8;
|
|
||||||
--surface-default: #F8F6F0;
|
|
||||||
--surface-raised: #FBF9F3;
|
|
||||||
--surface-muted: #E9E7E0;
|
|
||||||
--surface-sunken: #E5E3DC;
|
|
||||||
--surface-hover: #EEECE5;
|
|
||||||
--surface-selected: #E2E9E1;
|
|
||||||
--surface-disabled: #E7E5DF;
|
|
||||||
|
|
||||||
--sidebar-bg: #252722;
|
:root,
|
||||||
--sidebar-deep: #20221E;
|
[data-krona-theme="noble-green"],
|
||||||
--sidebar-surface: #2B2D27;
|
[data-theme="noble-green"],
|
||||||
--sidebar-hover: #30332C;
|
[data-theme="soft-light"] {
|
||||||
--sidebar-active: #34382F;
|
--bg-app: var(--krona-bg-app);
|
||||||
--sidebar-border: #3A3D35;
|
--bg-workspace: var(--krona-bg-workspace);
|
||||||
--sidebar-text: #D8D8D0;
|
--bg-section: var(--krona-bg-section);
|
||||||
--sidebar-text-secondary: #A9ACA3;
|
--surface-default: var(--krona-surface);
|
||||||
--sidebar-text-muted: #858980;
|
--surface-raised: var(--krona-surface-raised);
|
||||||
--sidebar-text-active: #F5F0E5;
|
--surface-muted: var(--krona-surface-muted);
|
||||||
--sidebar-icon: #BFC1B9;
|
--surface-sunken: var(--krona-surface-muted);
|
||||||
--sidebar-icon-active: #D4B16D;
|
--surface-hover: var(--krona-surface-hover);
|
||||||
--sidebar-indicator: #B78335;
|
--surface-selected: var(--krona-surface-selected);
|
||||||
|
--surface-disabled: var(--krona-surface-muted);
|
||||||
|
|
||||||
--text-primary: #252824;
|
--sidebar-bg: var(--krona-surface);
|
||||||
--text-secondary: #686C65;
|
--sidebar-deep: var(--krona-bg-app);
|
||||||
--text-tertiary: #7C8078;
|
--sidebar-surface: var(--krona-surface-raised);
|
||||||
--text-muted: #8A8D86;
|
--sidebar-hover: var(--krona-surface-hover);
|
||||||
--text-disabled: #AAACA6;
|
--sidebar-active: var(--krona-surface-selected);
|
||||||
--text-inverse: #F4F2EB;
|
--sidebar-border: var(--krona-border-subtle);
|
||||||
--text-brand: #805923;
|
--sidebar-text: var(--krona-text-primary);
|
||||||
--text-link: #2F684C;
|
--sidebar-text-secondary: var(--krona-text-secondary);
|
||||||
--text-link-hover: #214B37;
|
--sidebar-text-muted: var(--krona-text-muted);
|
||||||
|
--sidebar-text-active: var(--krona-primary);
|
||||||
|
--sidebar-icon: var(--krona-text-secondary);
|
||||||
|
--sidebar-icon-active: var(--krona-primary);
|
||||||
|
--sidebar-indicator: var(--krona-gold);
|
||||||
|
|
||||||
--border-subtle: #E3E0D7;
|
--text-primary: var(--krona-text-primary);
|
||||||
--border-default: #D8D5CB;
|
--text-secondary: var(--krona-text-secondary);
|
||||||
--border-strong: #C6C3B9;
|
--text-tertiary: var(--krona-text-secondary);
|
||||||
--border-focus: #5D8D6D;
|
--text-muted: var(--krona-text-muted);
|
||||||
|
--text-disabled: var(--krona-text-muted);
|
||||||
|
--text-inverse: var(--krona-text-inverse);
|
||||||
|
--text-brand: var(--krona-gold);
|
||||||
|
--text-link: var(--krona-primary);
|
||||||
|
--text-link-hover: var(--krona-primary-hover);
|
||||||
|
|
||||||
--color-green: #2F684C;
|
--border-subtle: var(--krona-border-subtle);
|
||||||
--color-green-hover: #285A42;
|
--border-default: var(--krona-border);
|
||||||
--color-green-active: #214B37;
|
--border-strong: var(--krona-border-strong);
|
||||||
--color-green-soft: #DDE9DF;
|
--border-focus: var(--krona-border-focus);
|
||||||
--color-green-border: #BDD4C3;
|
|
||||||
--color-green-text: #315F46;
|
|
||||||
|
|
||||||
--color-gold: #B78335;
|
--color-green: var(--krona-primary);
|
||||||
--color-gold-hover: #9E702D;
|
--color-green-hover: var(--krona-primary-hover);
|
||||||
--color-gold-active: #805923;
|
--color-green-active: var(--krona-primary-active);
|
||||||
--color-gold-soft: #EFE3CD;
|
--color-green-soft: var(--krona-primary-soft);
|
||||||
--color-gold-border: #DFC79C;
|
--color-green-border: var(--krona-border-strong);
|
||||||
--color-gold-text: #805923;
|
--color-green-text: var(--krona-primary);
|
||||||
|
|
||||||
--color-danger: #A6534E;
|
--color-gold: var(--krona-gold);
|
||||||
--color-danger-soft: #F1DFDC;
|
--color-gold-hover: var(--krona-gold-hover);
|
||||||
--color-danger-border: #DAB6B1;
|
--color-gold-active: var(--krona-gold-active);
|
||||||
--color-danger-text: #873F3B;
|
--color-gold-soft: var(--krona-gold-soft);
|
||||||
|
--color-gold-border: var(--krona-border);
|
||||||
|
--color-gold-text: var(--krona-gold);
|
||||||
|
|
||||||
--color-warning: #A8752F;
|
--color-danger: var(--krona-danger);
|
||||||
--color-warning-soft: #F2E7D3;
|
--color-danger-soft: var(--krona-danger-soft);
|
||||||
--color-warning-border: #DEC59B;
|
--color-danger-border: var(--krona-danger);
|
||||||
--color-warning-text: #80571F;
|
--color-danger-text: var(--krona-danger);
|
||||||
|
|
||||||
--color-info: #527286;
|
--color-warning: var(--krona-warning);
|
||||||
--color-info-soft: #DFE7EA;
|
--color-warning-soft: var(--krona-warning-soft);
|
||||||
--color-info-border: #BACBD1;
|
--color-warning-border: var(--krona-warning);
|
||||||
--color-info-text: #405F70;
|
--color-warning-text: var(--krona-warning);
|
||||||
|
|
||||||
|
--color-info: var(--krona-info);
|
||||||
|
--color-info-soft: var(--krona-info-soft);
|
||||||
|
--color-info-border: var(--krona-info);
|
||||||
|
--color-info-text: var(--krona-info);
|
||||||
|
|
||||||
/* Semantic aliases */
|
/* Semantic aliases */
|
||||||
--color-primary: var(--color-green);
|
--color-primary: var(--krona-primary);
|
||||||
--color-primary-hover: var(--color-green-hover);
|
--color-primary-hover: var(--krona-primary-hover);
|
||||||
--color-primary-active: var(--color-green-active);
|
--color-primary-active: var(--krona-primary-active);
|
||||||
--color-primary-soft: var(--color-green-soft);
|
--color-primary-soft: var(--krona-primary-soft);
|
||||||
--color-primary-text: var(--color-green-text);
|
--color-primary-text: var(--krona-text-inverse);
|
||||||
--color-accent: var(--color-gold);
|
--color-accent: var(--krona-gold);
|
||||||
--color-accent-hover: var(--color-gold-hover);
|
--color-accent-hover: var(--krona-gold-hover);
|
||||||
--color-accent-active: var(--color-gold-active);
|
--color-accent-active: var(--krona-gold-active);
|
||||||
--color-accent-soft: var(--color-gold-soft);
|
--color-accent-soft: var(--krona-gold-soft);
|
||||||
--color-surface: var(--surface-default);
|
--color-surface: var(--krona-surface);
|
||||||
--color-surface-raised: var(--surface-raised);
|
--color-surface-raised: var(--krona-surface-raised);
|
||||||
--color-surface-muted: var(--surface-muted);
|
--color-surface-muted: var(--krona-surface-muted);
|
||||||
--color-surface-sunken: var(--surface-sunken);
|
--color-surface-sunken: var(--krona-surface-muted);
|
||||||
--color-surface-hover: var(--surface-hover);
|
--color-surface-hover: var(--krona-surface-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-krona-theme="parchment-light"],
|
||||||
|
[data-theme="parchment-light"],
|
||||||
[data-theme="light"] {
|
[data-theme="light"] {
|
||||||
--bg-app: #F5F4EF;
|
--bg-app: var(--krona-bg-app);
|
||||||
--bg-workspace: #F8F7F2;
|
--bg-workspace: var(--krona-bg-workspace);
|
||||||
--bg-section: #F2F0E8;
|
--bg-section: var(--krona-bg-section);
|
||||||
--surface-default: #FCFAF5;
|
--surface-default: var(--krona-surface);
|
||||||
--surface-raised: #FFFFFF;
|
--surface-raised: var(--krona-surface-raised);
|
||||||
--surface-muted: #EFEEE8;
|
--surface-muted: var(--krona-surface-muted);
|
||||||
--surface-sunken: #E7E5DD;
|
--surface-sunken: var(--krona-surface-muted);
|
||||||
--surface-hover: #F2EFE8;
|
--surface-hover: var(--krona-surface-hover);
|
||||||
--surface-selected: #E2EAE1;
|
--surface-selected: var(--krona-surface-selected);
|
||||||
--surface-disabled: #EBE9E2;
|
--surface-disabled: var(--krona-surface-muted);
|
||||||
|
|
||||||
--sidebar-bg: #ECEAE3;
|
--sidebar-bg: var(--krona-surface);
|
||||||
--sidebar-deep: #E4E1D8;
|
--sidebar-deep: var(--krona-bg-app);
|
||||||
--sidebar-surface: #F2EFE8;
|
--sidebar-surface: var(--krona-surface-raised);
|
||||||
--sidebar-hover: #DFDDD4;
|
--sidebar-hover: var(--krona-surface-hover);
|
||||||
--sidebar-active: #D6D2C7;
|
--sidebar-active: var(--krona-surface-selected);
|
||||||
--sidebar-border: #D3CFCA;
|
--sidebar-border: var(--krona-border-subtle);
|
||||||
--sidebar-text: #3C4039;
|
--sidebar-text: var(--krona-text-primary);
|
||||||
--sidebar-text-secondary: #686C65;
|
--sidebar-text-secondary: var(--krona-text-secondary);
|
||||||
--sidebar-text-muted: #8A8D86;
|
--sidebar-text-muted: var(--krona-text-muted);
|
||||||
--sidebar-text-active: #1D201C;
|
--sidebar-text-active: var(--krona-primary);
|
||||||
--sidebar-icon: #555951;
|
--sidebar-icon: var(--krona-text-secondary);
|
||||||
--sidebar-icon-active: #B78335;
|
--sidebar-icon-active: var(--krona-primary);
|
||||||
--sidebar-indicator: #B78335;
|
--sidebar-indicator: var(--krona-gold);
|
||||||
|
|
||||||
--text-primary: #252824;
|
--text-primary: var(--krona-text-primary);
|
||||||
--text-secondary: #686C65;
|
--text-secondary: var(--krona-text-secondary);
|
||||||
--text-tertiary: #7C8078;
|
--text-tertiary: var(--krona-text-secondary);
|
||||||
--text-muted: #8A8D86;
|
--text-muted: var(--krona-text-muted);
|
||||||
--text-disabled: #AAACA6;
|
--text-disabled: var(--krona-text-muted);
|
||||||
--text-inverse: #F4F2EB;
|
--text-inverse: var(--krona-text-inverse);
|
||||||
--text-brand: #805923;
|
--text-brand: var(--krona-gold);
|
||||||
--text-link: #2F684C;
|
--text-link: var(--krona-primary);
|
||||||
--text-link-hover: #214B37;
|
--text-link-hover: var(--krona-primary-hover);
|
||||||
|
|
||||||
--border-subtle: #E8E5DD;
|
--border-subtle: var(--krona-border-subtle);
|
||||||
--border-default: #DDDAD1;
|
--border-default: var(--krona-border);
|
||||||
--border-strong: #C8C5BB;
|
--border-strong: var(--krona-border-strong);
|
||||||
--border-focus: #5D8D6D;
|
--border-focus: var(--krona-border-focus);
|
||||||
|
|
||||||
--color-green: #2F684C;
|
--color-green: var(--krona-primary);
|
||||||
--color-green-hover: #285A42;
|
--color-green-hover: var(--krona-primary-hover);
|
||||||
--color-green-active: #214B37;
|
--color-green-active: var(--krona-primary-active);
|
||||||
--color-green-soft: #E1EBE2;
|
--color-green-soft: var(--krona-primary-soft);
|
||||||
--color-green-border: #BDD4C3;
|
--color-green-border: var(--krona-border-strong);
|
||||||
--color-green-text: #315F46;
|
--color-green-text: var(--krona-primary);
|
||||||
|
|
||||||
--color-gold: #B78335;
|
--color-gold: var(--krona-gold);
|
||||||
--color-gold-hover: #9E702D;
|
--color-gold-hover: var(--krona-gold-hover);
|
||||||
--color-gold-active: #805923;
|
--color-gold-active: var(--krona-gold-active);
|
||||||
--color-gold-soft: #F4EAD8;
|
--color-gold-soft: var(--krona-gold-soft);
|
||||||
--color-gold-border: #DFC79C;
|
--color-gold-border: var(--krona-border);
|
||||||
--color-gold-text: #805923;
|
--color-gold-text: var(--krona-gold);
|
||||||
|
|
||||||
--color-danger: #A6534E;
|
--color-danger: var(--krona-danger);
|
||||||
--color-danger-soft: #F5E5E3;
|
--color-danger-soft: var(--krona-danger-soft);
|
||||||
--color-danger-border: #DAB6B1;
|
--color-danger-border: var(--krona-danger);
|
||||||
--color-danger-text: #873F3B;
|
--color-danger-text: var(--krona-danger);
|
||||||
|
|
||||||
--color-warning: #A8752F;
|
--color-warning: var(--krona-warning);
|
||||||
--color-warning-soft: #F7EEDB;
|
--color-warning-soft: var(--krona-warning-soft);
|
||||||
--color-warning-border: #DEC59B;
|
--color-warning-border: var(--krona-warning);
|
||||||
--color-warning-text: #80571F;
|
--color-warning-text: var(--krona-warning);
|
||||||
|
|
||||||
--color-info: #527286;
|
--color-info: var(--krona-info);
|
||||||
--color-info-soft: #E3EBEF;
|
--color-info-soft: var(--krona-info-soft);
|
||||||
--color-info-border: #BACBD1;
|
--color-info-border: var(--krona-info);
|
||||||
--color-info-text: #405F70;
|
--color-info-text: var(--krona-info);
|
||||||
|
|
||||||
/* Semantic aliases */
|
/* Semantic aliases */
|
||||||
--color-primary: var(--color-green);
|
--color-primary: var(--krona-primary);
|
||||||
--color-primary-hover: var(--color-green-hover);
|
--color-primary-hover: var(--krona-primary-hover);
|
||||||
--color-primary-active: var(--color-green-active);
|
--color-primary-active: var(--krona-primary-active);
|
||||||
--color-primary-soft: var(--color-green-soft);
|
--color-primary-soft: var(--krona-primary-soft);
|
||||||
--color-primary-text: var(--color-green-text);
|
--color-primary-text: var(--krona-text-inverse);
|
||||||
--color-accent: var(--color-gold);
|
--color-accent: var(--krona-gold);
|
||||||
--color-accent-hover: var(--color-gold-hover);
|
--color-accent-hover: var(--krona-gold-hover);
|
||||||
--color-accent-active: var(--color-gold-active);
|
--color-accent-active: var(--krona-gold-active);
|
||||||
--color-accent-soft: var(--color-gold-soft);
|
--color-accent-soft: var(--krona-gold-soft);
|
||||||
--color-surface: var(--surface-default);
|
--color-surface: var(--krona-surface);
|
||||||
--color-surface-raised: var(--surface-raised);
|
--color-surface-raised: var(--krona-surface-raised);
|
||||||
--color-surface-muted: var(--surface-muted);
|
--color-surface-muted: var(--krona-surface-muted);
|
||||||
--color-surface-sunken: var(--surface-sunken);
|
--color-surface-sunken: var(--krona-surface-muted);
|
||||||
--color-surface-hover: var(--surface-hover);
|
--color-surface-hover: var(--krona-surface-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-krona-theme="graphite-forest-dark"],
|
||||||
|
[data-theme="graphite-forest-dark"],
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
--bg-app: #1D1F1B;
|
--bg-app: var(--krona-bg-app);
|
||||||
--bg-workspace: #22241F;
|
--bg-workspace: var(--krona-bg-workspace);
|
||||||
--bg-section: #252822;
|
--bg-section: var(--krona-bg-section);
|
||||||
--surface-default: #292B26;
|
--surface-default: var(--krona-surface);
|
||||||
--surface-raised: #30322C;
|
--surface-raised: var(--krona-surface-raised);
|
||||||
--surface-muted: #252722;
|
--surface-muted: var(--krona-surface-muted);
|
||||||
--surface-sunken: #1F211D;
|
--surface-sunken: var(--krona-surface-muted);
|
||||||
--surface-hover: #343730;
|
--surface-hover: var(--krona-surface-hover);
|
||||||
--surface-selected: #2D3A30;
|
--surface-selected: var(--krona-surface-selected);
|
||||||
--surface-disabled: #2B2E28;
|
--surface-disabled: var(--krona-surface-muted);
|
||||||
|
|
||||||
--sidebar-bg: #191B18;
|
--sidebar-bg: var(--krona-surface);
|
||||||
--sidebar-deep: #151714;
|
--sidebar-deep: var(--krona-bg-app);
|
||||||
--sidebar-surface: #20221E;
|
--sidebar-surface: var(--krona-surface-raised);
|
||||||
--sidebar-hover: #272A24;
|
--sidebar-hover: var(--krona-surface-hover);
|
||||||
--sidebar-active: #2F332C;
|
--sidebar-active: var(--krona-surface-selected);
|
||||||
--sidebar-border: #2D302A;
|
--sidebar-border: var(--krona-border-subtle);
|
||||||
--sidebar-text: #B2B4AC;
|
--sidebar-text: var(--krona-text-primary);
|
||||||
--sidebar-text-secondary: #888C83;
|
--sidebar-text-secondary: var(--krona-text-secondary);
|
||||||
--sidebar-text-muted: #6E726A;
|
--sidebar-text-muted: var(--krona-text-muted);
|
||||||
--sidebar-text-active: #E8E6DE;
|
--sidebar-text-active: var(--krona-primary);
|
||||||
--sidebar-icon: #9FA29A;
|
--sidebar-icon: var(--krona-text-secondary);
|
||||||
--sidebar-icon-active: #C99A4D;
|
--sidebar-icon-active: var(--krona-gold);
|
||||||
--sidebar-indicator: #C99A4D;
|
--sidebar-indicator: var(--krona-gold);
|
||||||
|
|
||||||
--text-primary: #E8E6DE;
|
--text-primary: var(--krona-text-primary);
|
||||||
--text-secondary: #B2B4AC;
|
--text-secondary: var(--krona-text-secondary);
|
||||||
--text-tertiary: #888C83;
|
--text-tertiary: var(--krona-text-secondary);
|
||||||
--text-muted: #72766D;
|
--text-muted: var(--krona-text-muted);
|
||||||
--text-disabled: #585B53;
|
--text-disabled: var(--krona-text-muted);
|
||||||
--text-inverse: #1D1F1B;
|
--text-inverse: var(--krona-text-inverse);
|
||||||
--text-brand: #C99A4D;
|
--text-brand: var(--krona-gold);
|
||||||
--text-link: #6F9E7C;
|
--text-link: var(--krona-primary);
|
||||||
--text-link-hover: #8AB796;
|
--text-link-hover: var(--krona-primary-hover);
|
||||||
|
|
||||||
--border-subtle: #34372F;
|
--border-subtle: var(--krona-border-subtle);
|
||||||
--border-default: #3C3F37;
|
--border-default: var(--krona-border);
|
||||||
--border-strong: #4D5146;
|
--border-strong: var(--krona-border-strong);
|
||||||
--border-focus: #6F9E7C;
|
--border-focus: var(--krona-border-focus);
|
||||||
|
|
||||||
--color-green: #3D7A58;
|
--color-green: var(--krona-primary);
|
||||||
--color-green-hover: #488E67;
|
--color-green-hover: var(--krona-primary-hover);
|
||||||
--color-green-active: #2F684C;
|
--color-green-active: var(--krona-primary-active);
|
||||||
--color-green-soft: #233529;
|
--color-green-soft: var(--krona-primary-soft);
|
||||||
--color-green-border: #2D4C3A;
|
--color-green-border: var(--krona-border-strong);
|
||||||
--color-green-text: #8FC9A6;
|
--color-green-text: var(--krona-primary);
|
||||||
|
|
||||||
--color-gold: #C99A4D;
|
--color-gold: var(--krona-gold);
|
||||||
--color-gold-hover: #D7AB61;
|
--color-gold-hover: var(--krona-gold-hover);
|
||||||
--color-gold-active: #B78335;
|
--color-gold-active: var(--krona-gold-active);
|
||||||
--color-gold-soft: #382F1E;
|
--color-gold-soft: var(--krona-gold-soft);
|
||||||
--color-gold-border: #56472D;
|
--color-gold-border: var(--krona-border);
|
||||||
--color-gold-text: #E5C384;
|
--color-gold-text: var(--krona-gold);
|
||||||
|
|
||||||
--color-danger: #B85853;
|
--color-danger: var(--krona-danger);
|
||||||
--color-danger-soft: #382423;
|
--color-danger-soft: var(--krona-danger-soft);
|
||||||
--color-danger-border: #59312F;
|
--color-danger-border: var(--krona-danger);
|
||||||
--color-danger-text: #E58D89;
|
--color-danger-text: var(--krona-danger);
|
||||||
|
|
||||||
--color-warning: #B78335;
|
--color-warning: var(--krona-warning);
|
||||||
--color-warning-soft: #382F1E;
|
--color-warning-soft: var(--krona-warning-soft);
|
||||||
--color-warning-border: #56472D;
|
--color-warning-border: var(--krona-warning);
|
||||||
--color-warning-text: #DEAF62;
|
--color-warning-text: var(--krona-warning);
|
||||||
|
|
||||||
--color-info: #5B8096;
|
--color-info: var(--krona-info);
|
||||||
--color-info-soft: #212F38;
|
--color-info-soft: var(--krona-info-soft);
|
||||||
--color-info-border: #304856;
|
--color-info-border: var(--krona-info);
|
||||||
--color-info-text: #8EAFC2;
|
--color-info-text: var(--krona-info);
|
||||||
|
|
||||||
/* Semantic aliases */
|
/* Semantic aliases */
|
||||||
--color-primary: var(--color-green);
|
--color-primary: var(--krona-primary);
|
||||||
--color-primary-hover: var(--color-green-hover);
|
--color-primary-hover: var(--krona-primary-hover);
|
||||||
--color-primary-active: var(--color-green-active);
|
--color-primary-active: var(--krona-primary-active);
|
||||||
--color-primary-soft: var(--color-green-soft);
|
--color-primary-soft: var(--krona-primary-soft);
|
||||||
--color-primary-text: var(--color-green-text);
|
--color-primary-text: var(--krona-text-inverse);
|
||||||
--color-accent: var(--color-gold);
|
--color-accent: var(--krona-gold);
|
||||||
--color-accent-hover: var(--color-gold-hover);
|
--color-accent-hover: var(--krona-gold-hover);
|
||||||
--color-accent-active: var(--color-gold-active);
|
--color-accent-active: var(--krona-gold-active);
|
||||||
--color-accent-soft: var(--color-gold-soft);
|
--color-accent-soft: var(--krona-gold-soft);
|
||||||
--color-surface: var(--surface-default);
|
--color-surface: var(--krona-surface);
|
||||||
--color-surface-raised: var(--surface-raised);
|
--color-surface-raised: var(--krona-surface-raised);
|
||||||
--color-surface-muted: var(--surface-muted);
|
--color-surface-muted: var(--krona-surface-muted);
|
||||||
--color-surface-sunken: var(--surface-sunken);
|
--color-surface-sunken: var(--krona-surface-muted);
|
||||||
--color-surface-hover: var(--surface-hover);
|
--color-surface-hover: var(--krona-surface-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
|
||||||
|
|
@ -26,35 +26,38 @@ const validThemes: ThemeMode[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
function getInitialTheme(): ThemeMode {
|
function getInitialTheme(): ThemeMode {
|
||||||
if (typeof window === 'undefined') return 'soft-light';
|
if (typeof window === 'undefined') return 'parchment-light';
|
||||||
const saved = localStorage.getItem(STORAGE_KEY) as ThemeMode;
|
const saved = localStorage.getItem(STORAGE_KEY) as ThemeMode;
|
||||||
if (saved && validThemes.includes(saved)) {
|
if (saved && validThemes.includes(saved)) {
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
return 'soft-light';
|
return 'parchment-light';
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyThemeToDom(theme: ThemeMode) {
|
function applyThemeToDom(theme: ThemeMode) {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
const kronaThemeMap: Record<string, string> = {
|
const kronaThemeMap: Record<string, string> = {
|
||||||
'soft-light': 'noble-green',
|
|
||||||
'noble-green': 'noble-green',
|
|
||||||
'light': 'parchment-light',
|
|
||||||
'parchment-light': 'parchment-light',
|
'parchment-light': 'parchment-light',
|
||||||
'dark': 'graphite-forest-dark',
|
'noble-green': 'noble-green',
|
||||||
'graphite-forest-dark': 'graphite-forest-dark',
|
'graphite-forest-dark': 'graphite-forest-dark',
|
||||||
|
'light': 'parchment-light',
|
||||||
|
'soft-light': 'noble-green',
|
||||||
|
'dark': 'graphite-forest-dark',
|
||||||
};
|
};
|
||||||
const legacyThemeMap: Record<string, string> = {
|
const legacyThemeMap: Record<string, string> = {
|
||||||
'soft-light': 'soft-light',
|
|
||||||
'noble-green': 'soft-light',
|
|
||||||
'light': 'light',
|
|
||||||
'parchment-light': 'light',
|
'parchment-light': 'light',
|
||||||
'dark': 'dark',
|
'noble-green': 'soft-light',
|
||||||
'graphite-forest-dark': 'dark',
|
'graphite-forest-dark': 'dark',
|
||||||
|
'light': 'light',
|
||||||
|
'soft-light': 'soft-light',
|
||||||
|
'dark': 'dark',
|
||||||
};
|
};
|
||||||
|
|
||||||
document.documentElement.setAttribute('data-krona-theme', kronaThemeMap[theme] || 'noble-green');
|
const activeKrona = kronaThemeMap[theme] || 'parchment-light';
|
||||||
document.documentElement.setAttribute('data-theme', legacyThemeMap[theme] || 'soft-light');
|
const activeLegacy = legacyThemeMap[theme] || 'light';
|
||||||
|
|
||||||
|
document.documentElement.setAttribute('data-krona-theme', activeKrona);
|
||||||
|
document.documentElement.setAttribute('data-theme', activeLegacy);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useThemeStore = create<ThemeState>((set, get) => {
|
export const useThemeStore = create<ThemeState>((set, get) => {
|
||||||
|
|
@ -71,14 +74,16 @@ export const useThemeStore = create<ThemeState>((set, get) => {
|
||||||
toggleTheme: () => {
|
toggleTheme: () => {
|
||||||
const current = get().themeMode;
|
const current = get().themeMode;
|
||||||
const cycle: Record<string, ThemeMode> = {
|
const cycle: Record<string, ThemeMode> = {
|
||||||
|
// V3 Design System themes
|
||||||
|
'parchment-light': 'noble-green',
|
||||||
|
'noble-green': 'graphite-forest-dark',
|
||||||
|
'graphite-forest-dark': 'parchment-light',
|
||||||
|
// Legacy theme cycle (backward compatibility)
|
||||||
'soft-light': 'light',
|
'soft-light': 'light',
|
||||||
'light': 'dark',
|
'light': 'dark',
|
||||||
'dark': 'soft-light',
|
'dark': 'soft-light',
|
||||||
'noble-green': 'parchment-light',
|
|
||||||
'parchment-light': 'graphite-forest-dark',
|
|
||||||
'graphite-forest-dark': 'noble-green',
|
|
||||||
};
|
};
|
||||||
const next: ThemeMode = cycle[current] || 'soft-light';
|
const next: ThemeMode = cycle[current] || 'parchment-light';
|
||||||
get().setTheme(next);
|
get().setTheme(next);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export default defineConfig({
|
||||||
'react-dom': path.resolve(currentDir, './node_modules/react-dom'),
|
'react-dom': path.resolve(currentDir, './node_modules/react-dom'),
|
||||||
'@krona/tokens': path.resolve(currentDir, '../krona-library/packages/tokens/dist/index.mjs'),
|
'@krona/tokens': path.resolve(currentDir, '../krona-library/packages/tokens/dist/index.mjs'),
|
||||||
'@krona/themes': path.resolve(currentDir, '../krona-library/packages/themes/dist/index.mjs'),
|
'@krona/themes': path.resolve(currentDir, '../krona-library/packages/themes/dist/index.mjs'),
|
||||||
'@krona/ui': path.resolve(currentDir, '../krona-library/packages/ui/src/index.ts'),
|
'@krona/ui': path.resolve(currentDir, '../krona-library/packages/ui/dist/index.mjs'),
|
||||||
'@krona/tokens.css': path.resolve(currentDir, '../krona-library/packages/tokens/src/tokens.css'),
|
'@krona/tokens.css': path.resolve(currentDir, '../krona-library/packages/tokens/src/tokens.css'),
|
||||||
'@krona/themes.css': path.resolve(currentDir, '../krona-library/packages/themes/src/themes.css'),
|
'@krona/themes.css': path.resolve(currentDir, '../krona-library/packages/themes/src/themes.css'),
|
||||||
'@krona/ui.css': path.resolve(currentDir, '../krona-library/packages/ui/dist/style.css'),
|
'@krona/ui.css': path.resolve(currentDir, '../krona-library/packages/ui/dist/style.css'),
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export default defineConfig({
|
||||||
'react-dom': path.resolve(currentDir, './node_modules/react-dom'),
|
'react-dom': path.resolve(currentDir, './node_modules/react-dom'),
|
||||||
'@krona/tokens': path.resolve(currentDir, '../krona-library/packages/tokens/dist/index.mjs'),
|
'@krona/tokens': path.resolve(currentDir, '../krona-library/packages/tokens/dist/index.mjs'),
|
||||||
'@krona/themes': path.resolve(currentDir, '../krona-library/packages/themes/dist/index.mjs'),
|
'@krona/themes': path.resolve(currentDir, '../krona-library/packages/themes/dist/index.mjs'),
|
||||||
'@krona/ui': path.resolve(currentDir, '../krona-library/packages/ui/src/index.ts'),
|
'@krona/ui': path.resolve(currentDir, '../krona-library/packages/ui/dist/index.mjs'),
|
||||||
'@krona/tokens.css': path.resolve(currentDir, '../krona-library/packages/tokens/src/tokens.css'),
|
'@krona/tokens.css': path.resolve(currentDir, '../krona-library/packages/tokens/src/tokens.css'),
|
||||||
'@krona/themes.css': path.resolve(currentDir, '../krona-library/packages/themes/src/themes.css'),
|
'@krona/themes.css': path.resolve(currentDir, '../krona-library/packages/themes/src/themes.css'),
|
||||||
'@krona/ui.css': path.resolve(currentDir, '../krona-library/packages/ui/dist/style.css'),
|
'@krona/ui.css': path.resolve(currentDir, '../krona-library/packages/ui/dist/style.css'),
|
||||||
|
|
|
||||||