diff --git a/docs/mobile-production/README.md b/docs/mobile-production/README.md index 4bed0a922..9d34c6b8d 100644 --- a/docs/mobile-production/README.md +++ b/docs/mobile-production/README.md @@ -2,7 +2,9 @@ # Мобильный frontend-релиз без изменения desktop -Следующий production fix: [мобильные дефекты после #407](./fixes-20261010/README.md). +Актуальный production fix: [мобильные дефекты от 15.10](./fixes-20261015/README.md). + +Предыдущий production fix: [мобильные дефекты после #407](./fixes-20261010/README.md). Ниже сохранён исторический отчёт первоначального релиза #407. Дата проверки: 10.10.2026. Target: `master`, ветка `release/prod-mobile-responsive-20261010`. diff --git a/docs/mobile-production/fixes-20261015/README.md b/docs/mobile-production/fixes-20261015/README.md new file mode 100644 index 000000000..80ec77c01 --- /dev/null +++ b/docs/mobile-production/fixes-20261015/README.md @@ -0,0 +1,90 @@ + + +# Исправления мобильной production-версии + +Дата: 15.10.2026. Ветка: `fix/prod-mobile-polish-20261015`, target: `master`. +Baseline: `4bee93397da2fa2b06a5557b13496beda894921b` — master после merge #408. +Ветка основана непосредственно на master; другие изменения dev не включены. + +## Результат и acceptance audit + +Все новые стили ограничены шириной до 999px. Шаблоны desktop сохранены. +Runtime TypeScript, API endpoints/payloads, permissions, роли, дедлайны, правила оценки, +300ms debounce, фильтрация, pagination и autosave не менялись. Mont и production tokens сохранены. + +| AC | Требование | Реализация / проверка | Verdict | +| --- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| 1 | Публикация новости на своём месте | NewsForm: submit в footer, 44×44, рядом с вложениями; реальная отправка form через DOM и pending в unit test | PASS | +| 2 | Навыки читаются горизонтально | ProjectDirectionCard: контейнеры полной ширины, категории с wrap; Apache Kafka / hard / Back-end и длинные названия | PASS | +| 3 | Компактные оценки | RangeCriterionInput: 48×44, сокращён padding; 12px между критериями, длинные подписи переносятся; ввод 8 и blur | PASS | +| 4 | Рабочий фильтр оценок | Окно вынесено из desktop-only условия мобильной ветки; выбор кейса → URL → POST payload → список; повторное открытие и сброс; Escape из окна | PASS | +| 4a | Фильтр оценки без дополнительных полей | Переключатель виден и при пустом списке program fields; div вместо label предотвращает повторный синтетический click на checkbox | PASS | +| 5 | Аналитика курса в одну строку | Кнопка занимает ширину своей колонки; nowrap и отступы; 320/390/768px | PASS | +| 6 | Номера заданий по центру | Lesson: flex-центрирование, 44×44; 12 пунктов, включая двузначные номера; горизонтальный scroll | PASS | +| 7 | Создать проект в одну строку | Projects: ширина по содержимому и nowrap; при нехватке места заголовок/кнопка занимают отдельные строки | PASS | +| 7a | Две карточки в строке | Dashboard, списки проектов/участников и списки программы: две гибкие колонки; без изменения приглашений; тег навыка в карточке участника ограничен одной строкой | PASS | +| 7b | Пустая выдача остаётся широкой | Projects/all и Members: app-state занимает обе колонки, 320/390/768px | PASS | +| 8 | Панель новости не прижата к краю | FeedNewsModal: действия вынесены из scroll, нижний padding 24px + safe area, CTA виден | PASS | +| 9 | Пустые данные конкурса без грустного лица и наложения подсказки | ProjectAdditionalStep: удалён sad-smile в мобильной пустой ветке; кнопка и tooltip в одном flex-ряду, подсказка вне ссылки | PASS | +| 10 | Desktop сохраняет production | 109 точных RGB comparisons, 1000/1440/1920px; без masks/tolerance | PASS | + +## Evidence + +**Checked by source:** итоговый diff относительно master; геометрия только в max-width:999px; +старые desktop-ветки HTML, API и runtime TS сохранены. Review: actionable findings не осталось. +Проверены hierarchy, плотность двух колонок, длинный текст, empty/no-results, pending, +keyboard/названия controls и отсутствие побочных изменений. Существующие правила доступа не изменялись. + +**Checked by tests:** `npm run test:ci` — 413 файлов / 2103 теста, exit 0. +Добавлены четыре regression case: mobile filter projects/rating, rated checkbox без полей, +footer submit/pending. Исходные validation/permissions/service tests также прошли. + +**Checked by build:** `npm run build:prod` — exit 0; budgets не повышались. +Существующие CommonJS warnings сохраняются. Windows-команда build:sprite передаёт quoted glob некорректно; +для обеих visual-сборок восстановлен один и тот же tracked production sprite с symbol nodes. +CSS/JS bundles не патчились; исправление генератора sprite вне scope. + +**Static checks:** `npm run lint:ts` — 0 errors, 6 прежних unused-disable warnings; +Stylelint 16 изменённых SCSS — exit 0; `git diff --check` — exit 0. +`npm run format:check` проверяет полный staged index с LF, как на Linux CI. + +**Checked in browser:** Chrome/Windows, synthetic API без live mutations. +Mobile 320/390/768px: 9 новых состояний + выбранный фильтр/пустые program fields; +реальный select → query → POST → no results → reset, ввод оценки, 12 номеров урока, +две колонки и широкая пустая выдача. Основная прежняя матрица 16 страниц + 6 состояний также проходит. +Сохранены проверки разных ролей программы, уведомлений, редакторов, кнопок с иконками и приглашения. + +Desktop comparison: [66 основных состояний](./desktop-comparison.json), +[27 состояний текущих правок](./desktop-polish-comparison.json), +[16 дополнительных состояний](./desktop-layout-comparison.json). +В каждом сравнении 0 изменённых RGB pixels; viewport и данные одинаковые. +[Мобильные метрики](./mobile-polish.json), [empty/grid проверки](./grid-edge-cases.json). + +## Визуальный review + +[Отправка новости](./screenshots/composer-390.png) · [Навыки](./screenshots/skills-390.png) · +[Оценки](./screenshots/rating-390.png) · [Фильтр](./screenshots/filters-selected-390.png) · +[Курс](./screenshots/course-390.png) · [Урок](./screenshots/lesson-390.png) · +[Проекты](./screenshots/projects-grid-390.png) · [Участники](./screenshots/members-grid-390.png) · +[Новость](./screenshots/news-modal-390.png) · [Данные для конкурса](./screenshots/competition-empty-390.png). + +## Воспроизведение + +Используйте browser env из [основного отчёта](../README.md#воспроизведение-browser-gate). +Сначала соберите baseline указанного master SHA и сохраните dist отдельно, затем текущую ветку. +`QA_BUILD_ROOT` должен указывать на нужную сборку с полноценным sprite. + +`node docs/mobile-production/polish-regression.cjs baseline '320,390,768,1000,1440,1920' OUTPUT` + +`node docs/mobile-production/polish-regression.cjs verified '320,390,768,1000,1440,1920' OUTPUT` + +Для 27 desktop snapshots: compare.py с `--screens composer,skills,rating,course,lesson,projects-grid,members-grid,news-modal,competition-empty`. +Прежние visual.cjs (обычный + details) и layout-regression.cjs запускаются без изменения API harness. + +## Not checked / rollout + +Физические iOS/Android, Safari/WebKit, реальная клавиатура и live API в этой сессии не проверялись. +Pixel gate покрывает выбранные состояния, а не все страницы/данные приложения. +После merge на устройстве проверить публикацию новости, выбор/сброс кейса, длинные навыки, +scroll номеров урока, две колонки карточек и открытие подсказки конкурса. +Миграции и backend rollout не требуются. Merge/deploy выполняет пользователь. diff --git a/docs/mobile-production/fixes-20261015/desktop-comparison.json b/docs/mobile-production/fixes-20261015/desktop-comparison.json new file mode 100644 index 000000000..cc3c9bddf --- /dev/null +++ b/docs/mobile-production/fixes-20261015/desktop-comparison.json @@ -0,0 +1,530 @@ +[ + { + "screen": "analytics-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "analytics-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "analytics-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "analytics-attention-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "analytics-attention-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "analytics-attention-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "course-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "course-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "course-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "courses-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "courses-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "courses-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "feed-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "feed-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "feed-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "members-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "members-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "members-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "partners-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "partners-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "partners-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "profile-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "profile-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "profile-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "profile-edit-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "profile-edit-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "profile-edit-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "program-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "program-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "program-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "programs-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "programs-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "programs-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-fields-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-fields-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-fields-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-goals-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-goals-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-goals-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-new-goal-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-new-goal-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "project-edit-new-goal-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "projects-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "projects-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "projects-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "projects-invites-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "projects-invites-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "projects-invites-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "team-invite-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "team-invite-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "team-invite-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "team-invite-selected-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "team-invite-selected-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "team-invite-selected-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "vacancies-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "vacancies-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "vacancies-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "vacancy-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "vacancy-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "vacancy-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + } +] diff --git a/docs/mobile-production/fixes-20261015/desktop-layout-comparison.json b/docs/mobile-production/fixes-20261015/desktop-layout-comparison.json new file mode 100644 index 000000000..adab99084 --- /dev/null +++ b/docs/mobile-production/fixes-20261015/desktop-layout-comparison.json @@ -0,0 +1,130 @@ +[ + { + "screen": "achievements-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "achievements-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "contacts-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "contacts-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "controls-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "controls-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "create-project-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "create-project-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "notifications-empty-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "notifications-empty-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "notifications-loaded-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "notifications-loaded-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "organizer-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "organizer-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "participant-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "participant-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + } +] diff --git a/docs/mobile-production/fixes-20261015/desktop-polish-comparison.json b/docs/mobile-production/fixes-20261015/desktop-polish-comparison.json new file mode 100644 index 000000000..bfa67e59a --- /dev/null +++ b/docs/mobile-production/fixes-20261015/desktop-polish-comparison.json @@ -0,0 +1,218 @@ +[ + { + "screen": "competition-empty-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "competition-empty-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "competition-empty-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "composer-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "composer-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "composer-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "course-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "course-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "course-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "lesson-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "lesson-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "lesson-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "members-grid-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "members-grid-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "members-grid-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "news-modal-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "news-modal-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "news-modal-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "projects-grid-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "projects-grid-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "projects-grid-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "rating-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "rating-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "rating-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "skills-1000", + "beforeSize": [1000, 844], + "afterSize": [1000, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "skills-1440", + "beforeSize": [1440, 844], + "afterSize": [1440, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + }, + { + "screen": "skills-1920", + "beforeSize": [1920, 844], + "afterSize": [1920, 844], + "exactChangedPixels": 0, + "maxChannelDelta": 0, + "passed": true + } +] diff --git a/docs/mobile-production/fixes-20261015/grid-edge-cases.json b/docs/mobile-production/fixes-20261015/grid-edge-cases.json new file mode 100644 index 000000000..1850af15b --- /dev/null +++ b/docs/mobile-production/fixes-20261015/grid-edge-cases.json @@ -0,0 +1,140 @@ +[ + { + "route": "/office/projects/all", + "width": 320, + "positions": [ + { + "x": 12, + "y": 636, + "right": 154, + "height": 244 + }, + { + "x": 166, + "y": 636, + "right": 308, + "height": 244 + } + ], + "empty": { + "state": 296, + "list": 296 + }, + "passed": true + }, + { + "route": "/office/members", + "width": 320, + "positions": [ + { + "x": 16, + "y": 531, + "right": 154, + "height": 248 + }, + { + "x": 166, + "y": 531, + "right": 304, + "height": 248 + } + ], + "empty": { + "state": 288, + "list": 288 + }, + "passed": true + }, + { + "route": "/office/projects/all", + "width": 390, + "positions": [ + { + "x": 16, + "y": 558, + "right": 189, + "height": 244 + }, + { + "x": 201, + "y": 558, + "right": 374, + "height": 244 + } + ], + "empty": { + "state": 358, + "list": 358 + }, + "passed": true + }, + { + "route": "/office/members", + "width": 390, + "positions": [ + { + "x": 16, + "y": 531, + "right": 189, + "height": 248 + }, + { + "x": 201, + "y": 531, + "right": 374, + "height": 248 + } + ], + "empty": { + "state": 358, + "list": 358 + }, + "passed": true + }, + { + "route": "/office/projects/all", + "width": 768, + "positions": [ + { + "x": 16, + "y": 521.59375, + "right": 370.5, + "height": 244 + }, + { + "x": 382.5, + "y": 521.59375, + "right": 737, + "height": 244 + } + ], + "empty": { + "state": 721, + "list": 721 + }, + "passed": true + }, + { + "route": "/office/members", + "width": 768, + "positions": [ + { + "x": 16, + "y": 533, + "right": 370.5, + "height": 248 + }, + { + "x": 382.5, + "y": 533, + "right": 737, + "height": 248 + } + ], + "empty": { + "state": 721, + "list": 721 + }, + "passed": true + } +] diff --git a/docs/mobile-production/fixes-20261015/mobile-polish.json b/docs/mobile-production/fixes-20261015/mobile-polish.json new file mode 100644 index 000000000..537471a6c --- /dev/null +++ b/docs/mobile-production/fixes-20261015/mobile-polish.json @@ -0,0 +1,602 @@ +[ + { + "name": "composer", + "width": 320, + "button": { + "x": 251, + "y": 475.390625, + "width": 44, + "height": 44, + "right": 295, + "bottom": 519.390625 + }, + "footer": { + "x": 25, + "y": 465.390625, + "width": 270, + "height": 54, + "right": 295, + "bottom": 519.390625 + }, + "passed": true + }, + { + "name": "skills", + "width": 320, + "tags": [ + { + "text": "hard", + "w": 99, + "sw": 99, + "h": 24 + }, + { + "text": "Back-end", + "w": 129, + "sw": 129, + "h": 24 + }, + { + "text": "hard", + "w": 99, + "sw": 99, + "h": 24 + }, + { + "text": "Разработка программного обеспечения", + "w": 212, + "sw": 212, + "h": 55 + } + ], + "passed": true + }, + { + "name": "rating", + "width": 320, + "score": { + "x": 39, + "y": 437.09375, + "width": 48, + "height": 44, + "right": 87, + "bottom": 481.09375 + }, + "passed": true + }, + { + "name": "filters-selected", + "width": 320, + "passed": true + }, + { + "name": "filters-no-custom-fields", + "width": 320, + "passed": true + }, + { + "name": "course", + "width": 320, + "analytics": { + "x": 20, + "y": 244, + "width": 134, + "height": 44, + "right": 154, + "bottom": 288 + }, + "passed": true + }, + { + "name": "lesson", + "width": 320, + "alignment": [ + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + } + ], + "passed": true + }, + { + "name": "projects-grid", + "width": 320, + "boxes": [ + { + "x": 12, + "y": 475, + "width": 142, + "height": 309, + "right": 154, + "bottom": 784 + }, + { + "x": 166, + "y": 475, + "width": 142, + "height": 309, + "right": 308, + "bottom": 784 + } + ], + "passed": true + }, + { + "name": "members-grid", + "width": 320, + "boxes": [ + { + "x": 16, + "y": 531, + "width": 138, + "height": 248, + "right": 154, + "bottom": 779 + }, + { + "x": 166, + "y": 531, + "width": 138, + "height": 248, + "right": 304, + "bottom": 779 + } + ], + "passed": true + }, + { + "name": "news-modal", + "width": 320, + "padding": "24px", + "passed": true + }, + { + "name": "competition-empty", + "width": 320, + "passed": true + }, + { + "name": "composer", + "width": 390, + "button": { + "x": 317, + "y": 475.390625, + "width": 44, + "height": 44, + "right": 361, + "bottom": 519.390625 + }, + "footer": { + "x": 29, + "y": 465.390625, + "width": 332, + "height": 54, + "right": 361, + "bottom": 519.390625 + }, + "passed": true + }, + { + "name": "skills", + "width": 390, + "tags": [ + { + "text": "hard", + "w": 99, + "sw": 99, + "h": 24 + }, + { + "text": "Back-end", + "w": 129, + "sw": 129, + "h": 24 + }, + { + "text": "hard", + "w": 99, + "sw": 99, + "h": 24 + }, + { + "text": "Разработка программного обеспечения", + "w": 282, + "sw": 282, + "h": 39 + } + ], + "passed": true + }, + { + "name": "rating", + "width": 390, + "score": { + "x": 43, + "y": 444.5, + "width": 48, + "height": 44, + "right": 91, + "bottom": 488.5 + }, + "passed": true + }, + { + "name": "filters-selected", + "width": 390, + "passed": true + }, + { + "name": "filters-no-custom-fields", + "width": 390, + "passed": true + }, + { + "name": "course", + "width": 390, + "analytics": { + "x": 20, + "y": 244, + "width": 169, + "height": 44, + "right": 189, + "bottom": 288 + }, + "passed": true + }, + { + "name": "lesson", + "width": 390, + "alignment": [ + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + } + ], + "passed": true + }, + { + "name": "projects-grid", + "width": 390, + "boxes": [ + { + "x": 16, + "y": 415, + "width": 173, + "height": 309, + "right": 189, + "bottom": 724 + }, + { + "x": 201, + "y": 415, + "width": 173, + "height": 309, + "right": 374, + "bottom": 724 + } + ], + "passed": true + }, + { + "name": "members-grid", + "width": 390, + "boxes": [ + { + "x": 16, + "y": 531, + "width": 173, + "height": 248, + "right": 189, + "bottom": 779 + }, + { + "x": 201, + "y": 531, + "width": 173, + "height": 248, + "right": 374, + "bottom": 779 + } + ], + "passed": true + }, + { + "name": "news-modal", + "width": 390, + "padding": "24px", + "passed": true + }, + { + "name": "competition-empty", + "width": 390, + "passed": true + }, + { + "name": "composer", + "width": 768, + "button": { + "x": 680, + "y": 472.875, + "width": 44, + "height": 44, + "right": 724, + "bottom": 516.875 + }, + "footer": { + "x": 29, + "y": 462.875, + "width": 695, + "height": 54, + "right": 724, + "bottom": 516.875 + }, + "passed": true + }, + { + "name": "skills", + "width": 768, + "tags": [ + { + "text": "hard", + "w": 69, + "sw": 69, + "h": 24 + }, + { + "text": "Back-end", + "w": 99, + "sw": 99, + "h": 24 + }, + { + "text": "hard", + "w": 69, + "sw": 69, + "h": 24 + }, + { + "text": "Разработка программного обеспечения", + "w": 296, + "sw": 296, + "h": 24 + } + ], + "passed": true + }, + { + "name": "rating", + "width": 768, + "score": { + "x": 43, + "y": 631.9375, + "width": 48, + "height": 44, + "right": 91, + "bottom": 675.9375 + }, + "passed": true + }, + { + "name": "filters-selected", + "width": 768, + "passed": true + }, + { + "name": "filters-no-custom-fields", + "width": 768, + "passed": true + }, + { + "name": "course", + "width": 768, + "analytics": { + "x": 20, + "y": 244, + "width": 350.5, + "height": 44, + "right": 370.5, + "bottom": 288 + }, + "passed": true + }, + { + "name": "lesson", + "width": 768, + "alignment": [ + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0.0078125, + "y": 0 + }, + { + "x": 0, + "y": 0 + } + ], + "passed": true + }, + { + "name": "projects-grid", + "width": 768, + "boxes": [ + { + "x": 16, + "y": 412.59375, + "width": 354.5, + "height": 244, + "right": 370.5, + "bottom": 656.59375 + }, + { + "x": 382.5, + "y": 412.59375, + "width": 354.5, + "height": 244, + "right": 737, + "bottom": 656.59375 + } + ], + "passed": true + }, + { + "name": "members-grid", + "width": 768, + "boxes": [ + { + "x": 16, + "y": 533, + "width": 354.5, + "height": 248, + "right": 370.5, + "bottom": 781 + }, + { + "x": 382.5, + "y": 533, + "width": 354.5, + "height": 248, + "right": 737, + "bottom": 781 + } + ], + "passed": true + }, + { + "name": "news-modal", + "width": 768, + "padding": "24px", + "passed": true + }, + { + "name": "competition-empty", + "width": 768, + "passed": true + } +] diff --git a/docs/mobile-production/fixes-20261015/screenshots/competition-empty-390.png b/docs/mobile-production/fixes-20261015/screenshots/competition-empty-390.png new file mode 100644 index 000000000..9eed1007a Binary files /dev/null and b/docs/mobile-production/fixes-20261015/screenshots/competition-empty-390.png differ diff --git a/docs/mobile-production/fixes-20261015/screenshots/composer-390.png b/docs/mobile-production/fixes-20261015/screenshots/composer-390.png new file mode 100644 index 000000000..43313ca34 Binary files /dev/null and b/docs/mobile-production/fixes-20261015/screenshots/composer-390.png differ diff --git a/docs/mobile-production/fixes-20261015/screenshots/course-390.png b/docs/mobile-production/fixes-20261015/screenshots/course-390.png new file mode 100644 index 000000000..5edd9fe4a Binary files /dev/null and b/docs/mobile-production/fixes-20261015/screenshots/course-390.png differ diff --git a/docs/mobile-production/fixes-20261015/screenshots/filters-selected-390.png b/docs/mobile-production/fixes-20261015/screenshots/filters-selected-390.png new file mode 100644 index 000000000..fc827461e Binary files /dev/null and b/docs/mobile-production/fixes-20261015/screenshots/filters-selected-390.png differ diff --git a/docs/mobile-production/fixes-20261015/screenshots/lesson-390.png b/docs/mobile-production/fixes-20261015/screenshots/lesson-390.png new file mode 100644 index 000000000..26ccbbc2f Binary files /dev/null and b/docs/mobile-production/fixes-20261015/screenshots/lesson-390.png differ diff --git a/docs/mobile-production/fixes-20261015/screenshots/members-grid-390.png b/docs/mobile-production/fixes-20261015/screenshots/members-grid-390.png new file mode 100644 index 000000000..cfabcc541 Binary files /dev/null and b/docs/mobile-production/fixes-20261015/screenshots/members-grid-390.png differ diff --git a/docs/mobile-production/fixes-20261015/screenshots/news-modal-390.png b/docs/mobile-production/fixes-20261015/screenshots/news-modal-390.png new file mode 100644 index 000000000..71cbc06d8 Binary files /dev/null and b/docs/mobile-production/fixes-20261015/screenshots/news-modal-390.png differ diff --git a/docs/mobile-production/fixes-20261015/screenshots/projects-grid-390.png b/docs/mobile-production/fixes-20261015/screenshots/projects-grid-390.png new file mode 100644 index 000000000..4347eb5f0 Binary files /dev/null and b/docs/mobile-production/fixes-20261015/screenshots/projects-grid-390.png differ diff --git a/docs/mobile-production/fixes-20261015/screenshots/rating-390.png b/docs/mobile-production/fixes-20261015/screenshots/rating-390.png new file mode 100644 index 000000000..abbd9e87a Binary files /dev/null and b/docs/mobile-production/fixes-20261015/screenshots/rating-390.png differ diff --git a/docs/mobile-production/fixes-20261015/screenshots/skills-390.png b/docs/mobile-production/fixes-20261015/screenshots/skills-390.png new file mode 100644 index 000000000..67a494f4d Binary files /dev/null and b/docs/mobile-production/fixes-20261015/screenshots/skills-390.png differ diff --git a/docs/mobile-production/polish-regression.cjs b/docs/mobile-production/polish-regression.cjs new file mode 100644 index 000000000..d03f2eea0 --- /dev/null +++ b/docs/mobile-production/polish-regression.cjs @@ -0,0 +1,417 @@ +/** @format */ + +// Synthetic API only. Real Angular routes/components and unmodified production build bytes. +const fs = require("node:fs"); +const path = require("node:path"); +const assert = require("node:assert/strict"); +const { launch, assertViewport } = require("./browser-harness.cjs"); +const preview = require("./preview.cjs"); +const fixtures = require("./mobile-fixtures.cjs"); +const [phase, widthsArgument = "320,390,768,1000,1440,1920", directoryArgument] = + process.argv.slice(2); +const output = path.resolve(directoryArgument || ".release-mobile/fixes-20261015/polish"); +const widths = widthsArgument.split(",").map(Number); +const selected = process.env.QA_POLISH_SCREENS?.split(","); +const baseUrl = "http://127.0.0.1:4475"; +const check = phase !== "baseline"; +const pageOf = results => ({ count: results.length, next: null, previous: null, results }); +const original = fixtures.response; +const skill = { id: 1, name: "Apache Kafka", category: { id: 1, name: "Back-end" }, approves: [] }; +fixtures.user.skills = [ + skill, + { + ...skill, + id: 2, + name: "Проектирование распределённых систем", + category: { id: 2, name: "Разработка программного обеспечения" }, + }, +]; +fixtures.user.avatar = "/assets/images/profile/main.svg"; +fixtures.lesson.tasks = Array.from({ length: 12 }, (_, i) => ({ + ...fixtures.lesson.tasks[0], + id: i + 1, + order: i + 1, +})); +const news = { + id: 1, + name: "Алексей Куделько", + imageAddress: fixtures.user.avatar, + text: "Новости нашей команды", + datetimeCreated: "2026-10-01", + author: 1, + contentObject: { id: 1, email: "fixture@example.test" }, + files: [ + { + link: "/assets/images/profile/main.svg", + name: "Image", + mimeType: "image/svg+xml", + size: 0, + datetimeUploaded: "2026-10-01", + user: 1, + extension: "svg", + }, + ], + likesCount: 3, + viewsCount: 75, + isUserLiked: false, +}; +let noPrograms = false; +let filterMode = "configured"; +fixtures.response = (endpoint, method, query, state) => { + if (/\/feed\/$/.test(endpoint)) + return { + ...pageOf([{ typeModel: "news", content: news, publishedAt: "2026-10-01" }]), + counts: { all: 1, news: 1, project: 0, vacancy: 0, partnerprogram: 0, education: 0 }, + }; + if (/\/programs\/1\/filters\/$/.test(endpoint)) + return filterMode === "empty" + ? [] + : [ + { + id: 1, + name: "case", + label: "Кейс", + helpText: "Выберите кейс", + fieldType: "select", + isRequired: false, + options: ["Разработка", "Дизайн"], + }, + ]; + if (/\/rate-project\/1\/?$/.test(endpoint)) { + if (method === "POST" || Number(query.get("offset")) > 0) return pageOf([]); + const rating = original(endpoint, method, query, state).results[0]; + return pageOf([ + { + ...rating, + criterias: [ + "Новизна и оригинальность решения", + "Потенциал масштабирования", + "Глубина проработки", + "Качество материалов", + ].map((name, i) => ({ + id: i + 1, + name, + description: "", + type: "int", + minValue: 0, + maxValue: 10, + value: null, + expertId: null, + })), + }, + ]); + } + if (/\/projects\/1\/$/.test(endpoint) && noPrograms) + return { ...fixtures.project, partnerProgram: null, partnerPrograms: [] }; + const result = original(endpoint, method, query, state); + if (/\/auth\/public-users\/$/.test(endpoint)) + for (const member of result.results || []) { + member.avatar = fixtures.user.avatar; + if (member.personal) member.personal.avatar = fixtures.user.avatar; + } + if ( + /\/auth\/public-users\/$/.test(endpoint) || + /\/(projects|auth\/users\/projects)\/$/.test(endpoint) + ) { + if (result.results?.length) return pageOf([result.results[0], { ...result.results[0], id: 2 }]); + } + return result; +}; + +async function settle(page) { + await page.waitForLoadState("networkidle"); + await page.evaluate(async () => { + for (const image of document.images) image.loading = "eager"; + await document.fonts.ready; + await Promise.all([...document.fonts].map(font => font.load().catch(() => {}))); + await Promise.all([...document.images].map(image => image.decode().catch(() => {}))); + await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); + }); + await page.addStyleTag({ + content: + "*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}", + }); +} + +async function rect(locator) { + return locator.evaluate(el => { + const r = el.getBoundingClientRect(); + return { x: r.x, y: r.y, width: r.width, height: r.height, right: r.right, bottom: r.bottom }; + }); +} + +(async () => { + fs.mkdirSync(path.join(output, phase), { recursive: true }); + const rows = []; + for (const width of widths) { + const h = await launch(width, { baseUrl, isMobile: width < 750, hasTouch: width < 750 }); + await preview(h.context, baseUrl); + const p = h.page; + const mobile = check && width < 1000; + const shot = async (name, metrics = {}) => { + if (mobile) await assertViewport(p); + assert.deepEqual(h.errors, [], `${name}: console errors`); + await p.screenshot({ + path: path.join(output, phase, `${name}-${width}.png`), + fullPage: true, + }); + rows.push({ name, width, ...metrics, passed: true }); + console.log(`${phase}: ${name} ${width}`); + }; + const open = async (route, selector) => { + h.errors.length = 0; + await p.goto(baseUrl + route); + await p.locator(selector).first().waitFor({ state: "visible" }); + await settle(p); + }; + const run = name => !selected || selected.includes(name); + try { + await h.context.addInitScript(() => { + const NativeDate = Date; + window.Date = class extends NativeDate { + constructor(...args) { + super(...(args.length ? args : [1791288000000])); + } + static now() { + return NativeDate.now(); + } + }; + }); + if (run("composer")) { + await open("/office/projects/1", "app-news-form textarea"); + await p.locator("app-news-form textarea").fill("Сегодня был хороший день"); + await p.locator("app-news-form").scrollIntoViewIfNeeded(); + const send = p.locator("app-news-form .form__send"); + const button = await rect(send); + const footer = await rect(p.locator("app-news-form .footer")); + if (mobile) { + assert.equal(await send.getAttribute("type"), "submit"); + assert.ok(button.y >= footer.y && button.bottom <= footer.bottom + 1); + assert.ok(Math.abs(button.width - 44) < 1 && Math.abs(button.height - 44) < 1); + assert.equal(await send.isEnabled(), true); + } + await shot("composer", { button, footer }); + } + if (run("skills")) { + await open("/office/profile/1", "app-project-direction-card"); + await p + .locator("app-project-direction-card") + .filter({ hasText: "навыки" }) + .locator(".direction-card") + .click(); + await p.locator(".modal .skills__info").first().waitFor({ state: "visible" }); + await settle(p); + const tags = await p.locator(".modal .skills__category .tag").evaluateAll(els => + els.map(el => ({ + text: el.innerText, + w: el.clientWidth, + sw: el.scrollWidth, + h: el.clientHeight, + })), + ); + if (mobile) + for (const tag of tags) { + assert.ok(tag.h < 80, JSON.stringify(tag)); + assert.ok(tag.sw <= tag.w + 1, JSON.stringify(tag)); + } + await shot("skills", { tags }); + } + if (run("rating")) { + await open("/office/program/1/projects-rating", "app-range-criterion-input input"); + await p.locator("app-project-rating").first().scrollIntoViewIfNeeded(); + const input = p.locator("app-range-criterion-input input").first(); + const score = await rect(input); + const ratingCard = await rect(p.locator("app-rating-card .card").first()); + if (mobile) + assert.ok(ratingCard.x >= 0 && ratingCard.right <= width, JSON.stringify(ratingCard)); + if (mobile) { + assert.ok(score.width <= 52 && score.height === 44, JSON.stringify(score)); + const fields = await p.locator(".rating__field").evaluateAll(els => + els.map(el => { + const r = el.getBoundingClientRect(); + return { top: r.top, bottom: r.bottom }; + }), + ); + assert.ok(fields[1].top - fields[0].bottom >= 7); + await input.fill("8"); + await input.blur(); + assert.equal(await input.inputValue(), "8"); + } + await shot("rating", { score }); + } + if (run("filters") && width < 1000 && check) { + await open("/office/program/1/projects-rating", "app-rating-card"); + const trigger = p + .locator(".page__mobile-controls .page__mobile-filter-row app-button button") + .first(); + await trigger.click(); + await p.locator(".modal app-program-projects-filter").waitFor({ state: "visible" }); + await p.locator(".modal app-select").click(); + await p.getByText("Разработка", { exact: true }).last().click(); + await p.waitForURL(/case=/); + await p.waitForFunction(() => !document.querySelector("app-rating-card")); + assert.ok( + h.requests.some( + r => + r.method === "POST" && + /\/rate-project\/1/.test(r.path) && + JSON.parse(r.body).filters.case[0] === "Разработка", + ), + ); + await p.getByRole("button", { name: "применить фильтр", exact: true }).click(); + await p.locator(".modal app-program-projects-filter").waitFor({ state: "hidden" }); + await trigger.click(); + assert.equal( + await p + .locator(".modal app-select") + .innerText() + .then(x => x.includes("Разработка")), + true, + ); + await shot("filters-selected"); + await p.getByRole("button", { name: "применить фильтр", exact: true }).click(); + await p.locator(".page__clear").click(); + await p.waitForURL(/projects-rating$/); + await p.locator("app-rating-card").waitFor({ state: "visible" }); + await trigger.click(); + await p.getByRole("button", { name: "применить фильтр", exact: true }).focus(); + await p.keyboard.press("Escape"); + await p.locator(".modal app-program-projects-filter").waitFor({ state: "hidden" }); + filterMode = "empty"; + await open("/office/program/1/projects-rating", "app-rating-card"); + await p.locator(".page__mobile-controls app-button button").first().click(); + await p.locator(".modal .filter__tags").waitFor({ state: "visible" }); + await p.locator(".modal .filter__tags").click(); + await p.waitForURL(/is_rated_by_expert=true/); + await shot("filters-no-custom-fields"); + filterMode = "configured"; + } + if (run("course")) { + await open("/office/courses/1", ".info__actions"); + const button = p + .locator(".info__actions app-button") + .filter({ hasText: "аналитика" }) + .locator("button"); + const analytics = (await button.count()) ? await rect(button) : null; + if (mobile) { + assert.ok(analytics.height <= 46); + assert.equal(await button.evaluate(el => getComputedStyle(el).whiteSpace), "nowrap"); + } + await shot("course", { analytics }); + } + if (run("lesson")) { + await open("/office/courses/1/lesson/1", ".progress__task"); + const alignment = await p.locator(".progress__task").evaluateAll(els => + els.map(el => { + const a = el.getBoundingClientRect(), + b = el.querySelector("p").getBoundingClientRect(); + return { + x: Math.abs(a.x + a.width / 2 - b.x - b.width / 2), + y: Math.abs(a.y + a.height / 2 - b.y - b.height / 2), + }; + }), + ); + if (mobile) for (const a of alignment) assert.ok(a.x < 1 && a.y < 1, JSON.stringify(a)); + await shot("lesson", { alignment }); + } + for (const name of ["projects-grid", "members-grid"]) + if (run(name)) { + const projects = name === "projects-grid"; + await open( + projects ? "/office/projects/dashboard" : "/office/members", + projects ? "app-info-card" : "app-member-card", + ); + const cards = p.locator( + projects + ? "app-dashboard-item:first-child .dashboard__list app-info-card" + : ".page__list app-member-card", + ); + await cards.nth(1).waitFor({ state: "visible" }); + const boxes = [await rect(cards.nth(0)), await rect(cards.nth(1))]; + if (mobile) { + assert.ok(Math.abs(boxes[0].y - boxes[1].y) < 1, JSON.stringify(boxes)); + assert.ok(boxes[1].x - boxes[0].right >= 7); + assert.ok(boxes[0].width >= 130); + if (projects) { + const create = await rect(p.locator("app-page-header .bar__add-project button")); + assert.equal(create.height, 44); + } + } + await shot(name, { boxes }); + } + if (run("news-modal")) { + await open("/office/feed", "app-feed-news-preview"); + await p.locator(".feed-preview").first().click(); + await p.locator(".feed-news-modal__actions").waitFor({ state: "visible" }); + await settle(p); + const actions = p.locator(".feed-news-modal__actions"); + await actions.scrollIntoViewIfNeeded(); + const padding = await p + .locator(mobile ? ".feed-news-modal__bottom" : ".feed-news-modal__scroll") + .evaluate(el => getComputedStyle(el).paddingBottom); + if (mobile) { + const viewport = await rect(p.locator(".feed-news-modal__shell")); + const buttons = await rect(actions.locator("button").first()); + assert.ok(viewport.bottom - buttons.bottom >= 16, JSON.stringify({ viewport, buttons })); + assert.ok(parseFloat(padding) >= 24); + } + await shot("news-modal", { padding }); + } + if (run("competition-empty")) { + noPrograms = true; + await open("/office/projects/1/edit?editingStep=additional", "app-project-additional-step"); + await p + .getByText("Пока вы не участвуете ни в одной программе", { exact: true }) + .waitFor({ state: "visible" }); + await p.locator(".project__additional--no-items").scrollIntoViewIfNeeded(); + if (mobile) { + assert.equal( + await p.locator(".project__additional--no-items .project__no-items").count(), + 0, + ); + const tooltip = await rect(p.locator(".project__additional--no-items app-tooltip")); + const button = await rect(p.locator(".project__additional--no-items button")); + assert.ok(tooltip.x >= button.right + 7, JSON.stringify({ tooltip, button })); + const hint = p.locator(".project__additional--no-items app-tooltip"); + if (width < 750) await hint.click(); + else await hint.hover(); + await p + .getByText("Перейдите по ссылке и ознакомьтесь с тем, где можно сейчас поучаствовать", { + exact: true, + }) + .waitFor({ state: "visible" }); + } + await shot("competition-empty"); + noPrograms = false; + } + } catch (error) { + await p.screenshot({ + path: path.join(output, phase, `failure-${width}.png`), + fullPage: true, + }); + fs.writeFileSync( + path.join(output, phase, `failure-${width}.json`), + JSON.stringify( + { + url: p.url(), + errors: h.errors, + requests: h.requests.slice(-10), + text: await p.locator("body").innerText(), + }, + null, + 2, + ), + ); + throw error; + } finally { + await h.browser.close(); + } + } + fs.writeFileSync( + path.join(output, `${phase}${selected ? "-" + selected.join("-") : ""}.json`), + JSON.stringify(rows, null, 2), + ); +})().catch(error => { + console.error(error); + process.exitCode = 1; +}); diff --git a/projects/social_platform/src/app/ui/pages/courses/detail/course-detail.component.scss b/projects/social_platform/src/app/ui/pages/courses/detail/course-detail.component.scss index bfed9bc0c..110eb2537 100644 --- a/projects/social_platform/src/app/ui/pages/courses/detail/course-detail.component.scss +++ b/projects/social_platform/src/app/ui/pages/courses/detail/course-detail.component.scss @@ -1,3 +1,5 @@ +/** @format */ + @use "styles/responsive"; @use "styles/typography"; @@ -159,3 +161,17 @@ $detail-bar-mb: 12px; align-items: center; } } + +@media screen and (max-width: 999px) { + .info__actions:not(.info__actions--task) > app-button { + width: 100%; + min-width: 0; + + ::ng-deep button { + width: 100%; + min-width: 0; + padding-inline: 12px; + white-space: nowrap; + } + } +} diff --git a/projects/social_platform/src/app/ui/pages/courses/lesson/lesson.component.scss b/projects/social_platform/src/app/ui/pages/courses/lesson/lesson.component.scss index c1e7ddc99..259b17311 100644 --- a/projects/social_platform/src/app/ui/pages/courses/lesson/lesson.component.scss +++ b/projects/social_platform/src/app/ui/pages/courses/lesson/lesson.component.scss @@ -124,7 +124,7 @@ // Адаптив из dev применяется только ниже desktop breakpoint. @media screen and (max-width: 999px) { - @include responsive.apply-mobile { + @include responsive.apply-below-desktop { .task__actions { @include responsive.single-column; } @@ -145,8 +145,19 @@ } .progress__task { - min-width: 44px; - min-height: 44px; + display: flex; + flex: 0 0 44px; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + padding: 0; + line-height: 1; + + p { + margin: 0; + line-height: 1; + } } } } diff --git a/projects/social_platform/src/app/ui/pages/members/member-card/member-card.component.scss b/projects/social_platform/src/app/ui/pages/members/member-card/member-card.component.scss index 18123fb9c..38c368e5d 100644 --- a/projects/social_platform/src/app/ui/pages/members/member-card/member-card.component.scss +++ b/projects/social_platform/src/app/ui/pages/members/member-card/member-card.component.scss @@ -189,16 +189,14 @@ } .member-card__skill { - flex: 0 1 auto; - height: auto; - overflow-wrap: anywhere; - white-space: normal; + flex: 1 1 auto; + height: 22px; + white-space: nowrap; } .member-card__skills, .member-card__skills-area { - height: auto; - min-height: 22px; + height: 22px; } } } diff --git a/projects/social_platform/src/app/ui/pages/members/members.component.scss b/projects/social_platform/src/app/ui/pages/members/members.component.scss index 78e8e94e8..7d98fd838 100644 --- a/projects/social_platform/src/app/ui/pages/members/members.component.scss +++ b/projects/social_platform/src/app/ui/pages/members/members.component.scss @@ -171,9 +171,16 @@ } &__list { - grid-template-columns: repeat(auto-fill, minmax(0, var(--ui-mobile-entity-card-width))); + --ui-mobile-entity-card-width: 100%; + + grid-template-columns: repeat(2, minmax(0, 1fr)); + column-gap: 12px; justify-content: center; margin-top: 35px; + + > li:has(app-state) { + grid-column: 1 / -1; + } } } } diff --git a/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.html b/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.html index e77b52826..366532bab 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.html +++ b/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.html @@ -513,13 +513,17 @@ } + } + @if (listType() !== "members") { @defer (when isFilterOpen()) { + diff --git a/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.scss b/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.scss index 91acbf65d..87e8492ef 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.scss +++ b/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.scss @@ -325,9 +325,23 @@ padding: 0; } - @include responsive.apply-mobile { + @include responsive.apply-below-desktop { + .page__outlet { + min-width: 0; + } + .page__list { - @include responsive.single-column; + --ui-mobile-entity-card-width: 100%; + + grid-template-columns: repeat(2, minmax(0, 1fr)); + + &--rating { + grid-template-columns: minmax(0, 1fr); + } + + > li { + min-width: 0; + } } .page__mobile-filter-row { diff --git a/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.spec.ts index 661b0c7a8..8eb255df4 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/program/detail/list/list.component.spec.ts @@ -1,6 +1,6 @@ /** @format */ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, DeferBlockState, TestBed } from "@angular/core/testing"; import { provideRouter, Router, UrlTree } from "@angular/router"; import { signal } from "@angular/core"; import { of } from "rxjs"; @@ -14,6 +14,8 @@ import { ExportFileInfoService } from "@api/export-file/facades/export-file-info import { SwipeService } from "@api/swipe/swipe.service"; import { TooltipInfoService } from "@api/tooltip/tooltip-info.service"; +import { DesktopLayoutService } from "../../../../../../../../ui/src/lib/services/desktop-layout.service"; + import { Project } from "@domain/project/project.model"; import { projectCardFixture } from "@ui/widgets/info-card/info-card.fixture"; import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; @@ -25,9 +27,13 @@ describe("ProgramListComponent", () => { let component: ProgramListComponent; let fixture: ComponentFixture; const projects = signal([]); + const desktop = signal(true); + const listType = signal("projects"); beforeEach(async () => { projects.set([]); + desktop.set(true); + listType.set("projects"); const programDetailListInfoServiceSpy = { initializeSearchForm: vi.fn(), initializationListData: vi.fn(), @@ -38,7 +44,7 @@ describe("ProgramListComponent", () => { const fb = new FormBuilder(); const programDetailListUIInfoServiceSpy = { searchForm: fb.group({ search: [""] }), - listType: signal("projects"), + listType, searchedList: projects, profileProjSubsIds: signal([]), routerLink: vi.fn(), @@ -81,6 +87,7 @@ describe("ProgramListComponent", () => { imports: [ProgramListComponent], providers: [ provideRouter([]), + { provide: DesktopLayoutService, useValue: { desktop } }, { provide: IndustryRepositoryPort, useValue: { getOne: () => ({ name: "Образование" }) } }, { provide: AddProjectSubscriptionUseCase, @@ -154,4 +161,28 @@ describe("ProgramListComponent", () => { navigate.mockRestore(); } }); + it.each(["projects", "rating"])( + "opens the mobile %s filter and closes it with Escape", + async type => { + desktop.set(false); + listType.set(type); + (component as any).appWidth = 390; + fixture.detectChanges(); + fixture.nativeElement.querySelector(".page__mobile-controls app-button button").click(); + fixture.detectChanges(); + const blocks = await fixture.getDeferBlocks(); + expect(blocks).toHaveLength(1); + await blocks[0].render(DeferBlockState.Complete); + fixture.detectChanges(); + await new Promise(resolve => setTimeout(resolve)); + const dialog = document.querySelector( + '[role="dialog"][aria-labelledby="program-list-filter-title"]', + ); + expect(dialog?.querySelector("app-program-projects-filter")).not.toBeNull(); + dialog!.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); + fixture.detectChanges(); + await new Promise(resolve => setTimeout(resolve)); + expect(document.querySelector('[aria-labelledby="program-list-filter-title"]')).toBeNull(); + }, + ); }); diff --git a/projects/social_platform/src/app/ui/pages/program/detail/list/program-projects-filter/program-projects-filter.component.html b/projects/social_platform/src/app/ui/pages/program/detail/list/program-projects-filter/program-projects-filter.component.html index 0cba002af..e141ae5bf 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/list/program-projects-filter/program-projects-filter.component.html +++ b/projects/social_platform/src/app/ui/pages/program/detail/list/program-projects-filter/program-projects-filter.component.html @@ -79,7 +79,7 @@ cбросить - @if (filters()?.length) { + @if (filters()?.length || listType() === "rating") {
@if (filters()?.length && filterForm.controls) { @@ -136,12 +136,13 @@ } } @if (listType() === "rating") { -
diff --git a/projects/social_platform/src/app/ui/pages/program/detail/list/program-projects-filter/program-projects-filter.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/detail/list/program-projects-filter/program-projects-filter.component.spec.ts index ec1a1ed69..063e20f76 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/list/program-projects-filter/program-projects-filter.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/program/detail/list/program-projects-filter/program-projects-filter.component.spec.ts @@ -4,6 +4,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { provideRouter } from "@angular/router"; import { signal } from "@angular/core"; import { FormBuilder } from "@angular/forms"; +import { DesktopLayoutService } from "../../../../../../../../../ui/src/lib/services/desktop-layout.service"; import { ProgramProjectsFilterComponent } from "./program-projects-filter.component"; import { ProgramDetailListUIInfoService } from "@api/program/facades/detail/ui/program-detail-list-ui-info.service"; import { ProgramProjectsFilterInfoService } from "./service/program-projects-filter-info.service"; @@ -12,11 +13,16 @@ describe("ProjectsFilterComponent", () => { let component: ProgramProjectsFilterComponent; let fixture: ComponentFixture; const filters = signal([]); + const desktop = signal(true); + const listType = signal("projects"); const filterForm = new FormBuilder().group({ caseDirection: [null] }); beforeEach(async () => { + filters.set([]); + desktop.set(true); + listType.set("projects"); const programDetailListUIInfoServiceSpy = { - listType: signal("projects"), + listType, }; const programProjectsFilterInfoServiceSpy = { @@ -29,7 +35,7 @@ describe("ProjectsFilterComponent", () => { await TestBed.configureTestingModule({ imports: [ProgramProjectsFilterComponent], - providers: [provideRouter([])], + providers: [provideRouter([]), { provide: DesktopLayoutService, useValue: { desktop } }], }) .overrideComponent(ProgramProjectsFilterComponent, { remove: { @@ -82,4 +88,14 @@ describe("ProjectsFilterComponent", () => { ); expect(fixture.nativeElement.querySelector(".filter__select app-select")).not.toBeNull(); }); + it("shows the mobile rated-project filter when a program has no custom fields", () => { + desktop.set(false); + listType.set("rating"); + fixture.detectChanges(); + const toggle = fixture.nativeElement.querySelector(".filter__tags") as HTMLElement; + expect(toggle?.textContent).toContain("проекты с оценкой"); + toggle.click(); + const service = fixture.debugElement.injector.get(ProgramProjectsFilterInfoService); + expect(service.setValue).toHaveBeenCalledOnce(); + }); }); diff --git a/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/components/range-criterion-input/range-criterion-input.component.scss b/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/components/range-criterion-input/range-criterion-input.component.scss index b73adb14d..da69258ce 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/components/range-criterion-input/range-criterion-input.component.scss +++ b/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/components/range-criterion-input/range-criterion-input.component.scss @@ -72,11 +72,21 @@ input { } } - @include responsive.apply-mobile { + @include responsive.apply-below-desktop { + .field { + display: inline-flex; + gap: 4px; + align-items: center; + } + .field__input { - width: 44px; + width: 48px; height: 44px; - font-size: var(--ui-font-h3); + min-height: 44px; + padding: 4px 6px; + font-size: 16px; + line-height: 24px; + border-radius: var(--rounded-md); } } } diff --git a/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/project-rating.component.scss b/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/project-rating.component.scss index ae538640c..140925ee8 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/project-rating.component.scss +++ b/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/project-rating/project-rating.component.scss @@ -47,15 +47,22 @@ // Адаптив из dev применяется только ниже desktop breakpoint. @media screen and (max-width: 999px) { - @include responsive.apply-mobile { + @include responsive.apply-below-desktop { + .rating__columns { + gap: 12px; + } + .rating__input { flex-shrink: 0; } .rating__field label { + flex: 1; min-width: 0; + margin-top: 0; font-size: var(--ui-font-body); line-height: 20px; + overflow-wrap: anywhere; } .rating .rating__comment app-textarea ::ng-deep .field__input { diff --git a/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/rating-card.component.scss b/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/rating-card.component.scss index ea27b370b..982c8f465 100644 --- a/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/rating-card.component.scss +++ b/projects/social_platform/src/app/ui/pages/program/detail/list/rating-card/rating-card.component.scss @@ -231,8 +231,18 @@ // Адаптив из dev применяется только ниже desktop breakpoint. @media screen and (max-width: 999px) { - @include responsive.apply-mobile { + @include responsive.apply-below-desktop { + .card { + grid-template-columns: minmax(0, 1fr); + min-width: 0; + } + + .card__lower { + min-width: 0; + } + .card__wrapper { + min-width: 0; padding: 16px; } diff --git a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.scss b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.scss index d03c6bdfc..8498f7048 100644 --- a/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.scss +++ b/projects/social_platform/src/app/ui/pages/projects/dashboard/dashboardItem/dashboardItem.component.scss @@ -51,8 +51,15 @@ @media screen and (max-width: 999px) { @include responsive.apply-below-desktop { .dashboard__list { - grid-template-columns: repeat(auto-fill, minmax(0, var(--ui-mobile-entity-card-width))); + --ui-mobile-entity-card-width: 100%; + + grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 48px; + column-gap: 12px; + + > li { + min-width: 0; + } } .dashboard__top { diff --git a/projects/social_platform/src/app/ui/pages/projects/edit/components/project-additional-step/project-additional-step.component.html b/projects/social_platform/src/app/ui/pages/projects/edit/components/project-additional-step/project-additional-step.component.html index 3075962ac..828157117 100644 --- a/projects/social_platform/src/app/ui/pages/projects/edit/components/project-additional-step/project-additional-step.component.html +++ b/projects/social_platform/src/app/ui/pages/projects/edit/components/project-additional-step/project-additional-step.component.html @@ -413,26 +413,26 @@ } @else {

Пока вы не участвуете ни в одной программе

- - - программы - - - - - +
} } diff --git a/projects/social_platform/src/app/ui/pages/projects/edit/components/project-additional-step/project-additional-step.component.scss b/projects/social_platform/src/app/ui/pages/projects/edit/components/project-additional-step/project-additional-step.component.scss index f068dc549..feaa29ae0 100644 --- a/projects/social_platform/src/app/ui/pages/projects/edit/components/project-additional-step/project-additional-step.component.scss +++ b/projects/social_platform/src/app/ui/pages/projects/edit/components/project-additional-step/project-additional-step.component.scss @@ -179,6 +179,23 @@ // Адаптив из dev применяется только ниже desktop breakpoint. @media screen and (max-width: 999px) { + .project__additional--no-items { + display: flex; + width: 100%; + padding-block: 8px; + + app-tooltip { + position: static; + flex-shrink: 0; + } + } + + .project__programs-action { + display: flex; + gap: 12px; + align-items: center; + } + .invite { &__list { width: 100%; diff --git a/projects/social_platform/src/app/ui/pages/projects/list/list.component.scss b/projects/social_platform/src/app/ui/pages/projects/list/list.component.scss index 576c49287..7ccd01f86 100644 --- a/projects/social_platform/src/app/ui/pages/projects/list/list.component.scss +++ b/projects/social_platform/src/app/ui/pages/projects/list/list.component.scss @@ -179,8 +179,19 @@ @include responsive.apply-below-desktop { .page__list { - grid-template-columns: repeat(auto-fill, minmax(0, var(--ui-mobile-entity-card-width))); + --ui-mobile-entity-card-width: 100%; + + grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 48px; + column-gap: 12px; + + > li { + min-width: 0; + } + + > li:has(app-state) { + grid-column: 1 / -1; + } } .page__filter { diff --git a/projects/social_platform/src/app/ui/pages/projects/projects.component.scss b/projects/social_platform/src/app/ui/pages/projects/projects.component.scss index e36256560..b5448a63c 100644 --- a/projects/social_platform/src/app/ui/pages/projects/projects.component.scss +++ b/projects/social_platform/src/app/ui/pages/projects/projects.component.scss @@ -87,7 +87,17 @@ // Адаптив из dev применяется только ниже desktop breakpoint. @media screen and (max-width: 999px) { .bar__add-project { + width: max-content; margin-bottom: 0; + + ::ng-deep button { + width: max-content; + white-space: nowrap; + } + } + + .bar__add-project-text { + white-space: nowrap; } @include responsive.apply-below-desktop { diff --git a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss index 89c4dba81..96ecbe88a 100644 --- a/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss +++ b/projects/social_platform/src/app/ui/widgets/info-card/info-card.component.scss @@ -649,8 +649,8 @@ } .card__content { - padding-right: 16px; - padding-left: 16px; + padding-right: 12px; + padding-left: 12px; overflow: visible; } @@ -694,6 +694,14 @@ line-height: 16px; } + .card__access { + width: 100%; + } + + .card__access-label { + overflow-wrap: normal; + } + .card__description { flex-basis: auto; width: 100%; diff --git a/projects/social_platform/src/app/ui/widgets/news-card/feed-news-modal/feed-news-modal.component.html b/projects/social_platform/src/app/ui/widgets/news-card/feed-news-modal/feed-news-modal.component.html index f11f8759d..7f0864617 100644 --- a/projects/social_platform/src/app/ui/widgets/news-card/feed-news-modal/feed-news-modal.component.html +++ b/projects/social_platform/src/app/ui/widgets/news-card/feed-news-modal/feed-news-modal.component.html @@ -148,6 +148,8 @@ } } + +
@for (i of imagesList(); track i.id) { @@ -118,6 +109,14 @@ (change)="onInputFiles($event)" /> +
} diff --git a/projects/social_platform/src/app/ui/widgets/news-form/news-form.component.scss b/projects/social_platform/src/app/ui/widgets/news-form/news-form.component.scss index cd29ea123..c312b48a8 100644 --- a/projects/social_platform/src/app/ui/widgets/news-form/news-form.component.scss +++ b/projects/social_platform/src/app/ui/widgets/news-form/news-form.component.scss @@ -84,16 +84,40 @@ // Адаптив из dev применяется только ниже desktop breakpoint. @media screen and (max-width: 999px) { - @include responsive.apply-mobile { + @include responsive.apply-below-desktop { .form__input { min-width: 0; } .form__send { - flex-shrink: 0; - min-width: 44px; - min-height: 44px; - margin-left: 8px; + display: inline-flex; + flex: 0 0 44px; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + padding: 0; + margin-left: auto; + color: var(--white); + background: var(--accent); + border: 0; + border-radius: var(--rounded-md); + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 2px; + } + } + + .footer { + display: flex; + gap: 12px; + align-items: center; } .footer__attach { diff --git a/projects/social_platform/src/app/ui/widgets/news-form/news-form.component.spec.ts b/projects/social_platform/src/app/ui/widgets/news-form/news-form.component.spec.ts index a105eb0a3..bb56bfa67 100644 --- a/projects/social_platform/src/app/ui/widgets/news-form/news-form.component.spec.ts +++ b/projects/social_platform/src/app/ui/widgets/news-form/news-form.component.spec.ts @@ -1,5 +1,7 @@ /** @format */ +import { signal } from "@angular/core"; +import { DesktopLayoutService } from "../../../../../../ui/src/lib/services/desktop-layout.service"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ReactiveFormsModule } from "@angular/forms"; import { HttpClientTestingModule } from "@angular/common/http/testing"; @@ -38,6 +40,7 @@ describe("NewsFormComponent", () => { NewsFormComponent, ], providers: [ + { provide: DesktopLayoutService, useValue: { desktop: signal(false) } }, { provide: ProjectNewsService, useValue: projectNewsServiceSpy }, { provide: AuthRepository, useValue: authSpy }, { provide: FileService, useValue: fileService }, @@ -202,6 +205,22 @@ describe("NewsFormComponent", () => { files: ["https://cdn.example.com/doc.pdf"], }); }); + it("submits through the mobile footer button once and disables it while pending", () => { + const emit = vi.spyOn(component.addNews, "emit"); + component.messageForm.patchValue({ text: "Новости команды" }); + fixture.detectChanges(); + const button = fixture.nativeElement.querySelector( + '.footer button[type="submit"]', + ) as HTMLButtonElement; + expect(button.getAttribute("aria-label")).toBe("Опубликовать новость"); + button.click(); + expect(emit).toHaveBeenCalledExactlyOnceWith({ text: "Новости команды", files: [] }); + fixture.componentRef.setInput("pending", true); + fixture.detectChanges(); + expect(button.disabled).toBe(true); + button.click(); + expect(emit).toHaveBeenCalledOnce(); + }); }); function createFile(name: string, type: string): File { diff --git a/projects/social_platform/src/app/ui/widgets/project-direction-card/project-direction-card.component.scss b/projects/social_platform/src/app/ui/widgets/project-direction-card/project-direction-card.component.scss index b106256c6..fc0371bac 100644 --- a/projects/social_platform/src/app/ui/widgets/project-direction-card/project-direction-card.component.scss +++ b/projects/social_platform/src/app/ui/widgets/project-direction-card/project-direction-card.component.scss @@ -251,6 +251,53 @@ } @media screen and (max-width: 999px) { + .cancel { + width: 100%; + height: auto; + min-height: 160px; + } + + .cancel__skills, + .cancel__skills--block { + width: 100%; + min-width: 0; + } + + .cancel__skills--block { + display: grid; + gap: 12px; + } + + .skills__info { + min-width: 0; + padding: 12px; + overflow-wrap: anywhere; + } + + .skills__category { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; + + app-tag { + flex: 0 1 auto; + width: auto; + min-width: 0; + max-width: 100%; + } + + ::ng-deep .tag { + width: auto; + max-width: 100%; + padding: 4px 10px; + + p { + margin: 0; + } + } + } + :host { display: block; min-width: 0;