diff --git a/docs/prod-feed-news/README.md b/docs/prod-feed-news/README.md new file mode 100644 index 000000000..4ea1cbc85 --- /dev/null +++ b/docs/prod-feed-news/README.md @@ -0,0 +1,179 @@ + + +# Браузерная приёмка PROD-порта новостной ленты + +## Контекст + +Проверено 24 сентября 2026 года в локальном Chromium на итоговой рабочей копии selective semantic port в ветке `codex/prod-news-feed-angular`. + +- PROD base: `8f0202ab0937965bb548a93ca32c7fc6ff0e9242` (`origin/master`). +- Финальный DEV source: `bdb5cf4780be0daa3e441d6b152511fcbe638d4e` после #375. +- Источники Angular: #372 `d4708cbc…`, #373 `e2c6d00d…`, #374 `75f206da…`, #375 `bdb5cf47…`. +- Browser: Chrome 153.0.8010.53, viewport scale 100%, `devicePixelRatio=1`. +- На момент приёмки port оставался незакоммиченным согласно ограничению задачи; merge и deploy не выполнялись. + +Итог: **browser acceptance GREEN с перечисленными ниже границами локального fixture**. + +## Как устроена проверка + +В игнорируемом `tmp/` собран отдельный Angular entry point на основе финального DEV fixture. В нём работают настоящие `FeedComponent`, `FeedInfoService`, `FeedFilter`, четыре вида карточек, `NewsCard`, carousel и modal из текущего PROD-порта. Сеть заменена локальными use case с 12 синтетическими элементами; runtime/config/workflows/dependencies приложения не менялись. + +Первая страница содержит: + +1. короткую новость человека без изображения; +2. короткую новость с пятью изображениями; +3. длинную новость проекта без изображения; +4. вакансию; +5. новый проект; +6. новость проекта с изображением и файлом. + +Вторая страница содержит ещё шесть элементов с уникальными составными ключами. Для modal использованы локальные SVG `600×900`, `600×600`, `800×600`, `960×540` и `1500×300`. + +Fixture использует один внутренний scrollbar office-shell. Дополнительный зарезервированный gutter документа отключён только в локальном harness, чтобы не создавать искусственный второй scrollbar; production CSS не менялся. + +## Viewport и сетка + +| Viewport | Колонки | Ширина карточки, px | Высоты карточек/рядов, px | Horizontal overflow | +| ------------------- | ------: | ------------------: | ----------------------------------- | ------------------: | +| 1440 × 900 | 2 | 423,328 | 267,5 / 308 / 350 | 0 | +| 1280 × 900 | 2 | 423,328 | 267,5 / 308 / 350 | 0 | +| 1024 × 900 | 2 | 368,75 | 267,5 / 328 / 350 | 0 | +| 390 × 844 | 1 | 375 | 222 / 267,5 / 306 / 326 / 276 / 350 | 0 | +| 1440 × 900, extreme | 2 | 423,328 | 328 / 348 / 350 | 0 | + +На desktop разница высот и Y-позиций CTA внутри каждого CSS Grid row равна `0 px`. Следующие ряды имеют собственную высоту: глобальная высота `420px` не вернулась. Порядок DOM остаётся построчным `1 2 / 3 4 / 5 6`. + +Thumbnail на всех четырёх ширинах — `120 × 67,5 px`, `object-fit: cover`. Короткая новость без изображения не создаёт media-slot и не показывает «Подробнее»; длинная новость показывает кнопку по фактическому DOM-обрезанию. + +Сырые координаты карточек, CTA, фильтров, thumbnail и мета-строк находятся в [geometry.json](geometry.json). + +## Фильтры и counts + +| Категория | Count | +| ------------------ | ----: | +| Все новости | 12 | +| Новости проектов | 3 | +| Свежие вакансии | 3 | +| Новости сообщества | 6 | +| Новости программ | 0 | +| Образование | 0 | + +На 1440/1280/1024 все шесть чисел имеют одну Y-координату, `14px / 600`, line-height `18px`, tabular nums. На 390 px они совпадают внутри каждой двухколоночной строки. Открытое mobile-меню также проверено. + +Клик «Новости сообщества»: + +- ставит `aria-pressed=true` только выбранной кнопке; +- вызывает локальный feed use case с `offset=0`, `limit=6`, `type=news`; +- оставляет шесть элементов `news`; +- не меняет глобальные counts. + +Standalone TestBed router не считается проверкой синхронизации production address bar: активное состояние, query stream и fetch проверены, но после синтетического одно-route перехода итоговый `location.search` пустой. + +## Пагинация 6 → 12 + +До прокрутки в DOM находятся шесть элементов. Реальный обработчик `FeedInfoService.initScroll` после scroll-события вызвал: + +```text +offset=6, limit=6, type=vacancy|project|news +``` + +После ответа в DOM 12 элементов. Первые шесть ключей сохранены без перестановки, новые добавлены в конец, все ключи уникальны. Horizontal overflow после append также равен `0`. + +## News preview и нижняя мета-строка + +- Badges и даты `23.09.2026` отображаются для четырёх типов. +- Industry проекта находится в верхней badge/meta-зоне; в footer её нет. +- Like/count и share находятся слева, views/count — справа. +- Все три иконки имеют `16 × 16 px`. +- Числа имеют `12px`, line-height `16px`, tabular nums. +- Максимальное расхождение центров элементов по вертикали — `0 px` в preview (`0,008 px` в modal из-за субпиксельного округления). +- Левая и правая группы имеют одинаковые геометрические inset `21 px` относительно внешней рамки карточки (`20 px` padding + `1 px` border). + +Клики like, share, source и CTA не открывают modal. Для source/CTA в одно-route fixture подавлялась только фактическая навигация; card click-handler получил настоящий browser event. Owner dots невозможно показать через текущий `FeedComponent`, потому что global feed не передаёт `isOwner`; исключение owner menu из открытия modal остаётся покрыто component regression test. + +## Modal и изображения + +| Сценарий | Modal body, px | Natural | Видимый кадр, px | Fit | +| ------------------- | -------------- | ---------- | ---------------- | ------- | +| Portrait, 1440 | 880 × 810 | 600 × 900 | 408 × 612 | contain | +| Square, 1440 | 880 × 810 | 600 × 600 | 612 × 612 | contain | +| Landscape 4:3, 1440 | 880 × 810 | 800 × 600 | 816 × 612 | contain | +| Wide 16:9, 1440 | 880 × 778,672 | 960 × 540 | 840 × 472,5 | contain | +| Panorama, 1440 | 880 × 474,172 | 1500 × 300 | 840 × 168 | contain | +| Portrait, 390 | 366 × 759,594 | 600 × 900 | 319 × 478,5 | contain | + +Разница natural/painted aspect ratio во всех пяти случаях равна `0`; crop отсутствует. Стрелками последовательно проверены индексы `1/5`–`5/5`. Modal с файлом сохраняет `href=/fixtures/notes.txt` и `download="Описание проекта.txt"`. + +Extreme mobile modal имеет `scrollHeight=8037`, `clientHeight=703`; в нижнем состоянии `scrollTop=7334`. Header и крестик сохраняют координаты (`header Y=42,203`, close Y=`52,203`) и не уезжают с содержимым. + +## Интеракции + +- Мышь по thumbnail открывает modal. +- «Подробнее» длинной новости без изображения открывает полный текст; у короткой новости кнопки нет. +- `Enter` и `Space` на карточке открывают modal. +- Escape, крестик и backdrop закрывают его. +- После каждого закрытия фокус возвращается на исходную карточку/кнопку. +- Like в preview: `234 / true → 233 / false`, modal не открывается. +- Like в modal: `234 / true → 233 / false` одновременно в modal и лежащем под ним preview; зафиксирован один вызов прежнего like flow. +- Share в preview и modal не открывает новое окно; локальная clipboard-заглушка получила ожидаемые feed URL. +- Открытие modal не вызвало feed/detail fetch: используется уже загруженный `FeedNews`. +- Console errors: `0`; console warnings: `0`. + +## Скриншоты + +Основная лента и размеры: + +- [Вся лента 1440](screenshots/feed-all-1440.png) +- [Viewport 1440](screenshots/feed-1440.png) +- [Viewport 1280](screenshots/feed-1280.png) +- [Viewport 1024](screenshots/feed-1024.png) +- [Viewport 390](screenshots/feed-390.png) + +Фильтры и карточки: + +- [Фильтры 1440](screenshots/filters-1440.png) +- [Активный фильтр новостей](screenshots/filter-news-active.png) +- [Открытый mobile-фильтр](screenshots/filters-390-open.png) +- [Вакансия](screenshots/vacancy-1440.png) +- [Длинная новость](screenshots/long-news-1440.png) +- [Extreme long news](screenshots/long-news-extreme-1440.png) +- [Новость с изображением](screenshots/image-news-1440.png) + +Пагинация: + +- [До append: 6](screenshots/pagination-6.png) +- [После append: 12](screenshots/pagination-12.png) + +Modal: + +- [Portrait](screenshots/modal-portrait-1440.png) +- [Landscape](screenshots/modal-landscape-1440.png) +- [Пять изображений, индекс 5/5](screenshots/modal-multiple-images-1440.png) +- [Файл в modal](screenshots/modal-file-1440.png) +- [Mobile modal](screenshots/modal-390.png) +- [Mobile extreme, начало](screenshots/modal-390-long-top.png) +- [Mobile extreme, конец](screenshots/modal-390-long-bottom.png) + +## Границы приёмки + +- Live backend, авторизация, реальные данные и доступность целевых detail/profile routes не проверялись. +- Системный download не запускался; проверены DOM-атрибуты `href` и `download`. +- Запись в системный clipboard не выполнялась: локальная заглушка подтвердила вызов обработчика и сформированный URL. +- Owner menu не выводится самим global `FeedComponent`; его browser-сценарий объективно недоступен без изменения runtime-входов, поэтому он оставлен regression-тестам. +- Browser fixture — development build для визуальной проверки. Production build и test suites фиксируются отдельно в общем отчёте PR. + +## Воспроизведение + +Локальный harness остаётся только в игнорируемом `tmp/`. Использованные шаги: + +```powershell +node_modules\.bin\ng.cmd build social_platform --configuration=development ` + --browser tmp/surface-main.ts ` + --ts-config tmp/surface-tsconfig.json ` + --index tmp/surface-index.html ` + --output-path tmp/preview + +# Затем browser/ обслуживался локальным SPA-сервером на 127.0.0.1. +``` + +Runtime-код, Angular config, package manifests, test harness и workflows этой browser-проверкой не менялись. diff --git a/docs/prod-feed-news/geometry.json b/docs/prod-feed-news/geometry.json new file mode 100644 index 000000000..dc29d3efc --- /dev/null +++ b/docs/prod-feed-news/geometry.json @@ -0,0 +1,6015 @@ +{ + "schemaVersion": 1, + "generatedAt": "2026-09-24T11:27:57.297Z", + "source": { + "prodBaseSha": "8f0202ab0937965bb548a93ca32c7fc6ff0e9242", + "sourceDevSha": "bdb5cf4780be0daa3e441d6b152511fcbe638d4e", + "testedGitHead": "8f0202ab0937965bb548a93ca32c7fc6ff0e9242", + "testedState": "незакоммиченный selective semantic port в codex/prod-news-feed-angular", + "fixture": "локальный Angular TestBed, реальные FeedComponent/FeedInfoService/NewsCard/Modal, синтетические данные и fake use cases без сети", + "browser": { + "name": "Chrome/153.0.8010.53", + "protocol": "1.3", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36" + } + }, + "thresholds": { + "horizontalOverflowPx": 0, + "sameRowHeightDeltaMaxPx": 0.5, + "sameRowCtaYDeltaMaxPx": 0.5, + "actionCenterlineDeltaMaxPx": 0.5, + "iconSizePx": 16, + "metaNumberFontSizePx": 12 + }, + "feed": [ + { + "label": "feed-1440", + "viewport": { + "width": 1440, + "height": 900, + "dpr": 1 + }, + "horizontalOverflowPx": 0, + "overflowCandidates": [ + { + "selector": "html", + "clientWidth": 1440, + "scrollWidth": 1440, + "overflow": 0 + }, + { + "selector": "body", + "clientWidth": 1440, + "scrollWidth": 1440, + "overflow": 0 + }, + { + "selector": "office", + "clientWidth": 1440, + "scrollWidth": 1440, + "overflow": 0 + }, + { + "selector": "office__body", + "clientWidth": 1425, + "scrollWidth": 1425, + "overflow": 0 + }, + { + "selector": "office__inner--content", + "clientWidth": 867, + "scrollWidth": 867, + "overflow": 0 + }, + { + "selector": "page", + "clientWidth": 867, + "scrollWidth": 867, + "overflow": 0 + }, + { + "selector": "page__feed", + "clientWidth": 867, + "scrollWidth": 867, + "overflow": 0 + } + ], + "officeScroll": { + "top": 0, + "height": 900, + "total": 1173 + }, + "grid": { + "columns": "423.328px 423.328px", + "columnGap": "20px", + "rowGap": "20px" + }, + "cards": [ + { + "key": "news:1", + "type": "Новости людей", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 365.828, + "y": 207, + "width": 423.328, + "height": 267.5, + "right": 789.156, + "bottom": 474.5, + "centerY": 340.75 + }, + "cta": { + "x": 386.828, + "y": 417.5, + "width": 381.328, + "height": 36, + "right": 768.156, + "bottom": 453.5, + "centerY": 435.5 + } + }, + { + "key": "news:2", + "type": "Новости людей", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": true, + "hasMediaSlot": true, + "rect": { + "x": 809.156, + "y": 207, + "width": 423.328, + "height": 267.5, + "right": 1232.484, + "bottom": 474.5, + "centerY": 340.75 + }, + "cta": { + "x": 830.156, + "y": 417.5, + "width": 381.328, + "height": 36, + "right": 1211.484, + "bottom": 453.5, + "centerY": 435.5 + } + }, + { + "key": "news:3", + "type": "Новости проекта", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 365.828, + "y": 494.5, + "width": 423.328, + "height": 308, + "right": 789.156, + "bottom": 802.5, + "centerY": 648.5 + }, + "cta": { + "x": 386.828, + "y": 745.5, + "width": 381.328, + "height": 36, + "right": 768.156, + "bottom": 781.5, + "centerY": 763.5 + } + }, + { + "key": "vacancy:4", + "type": "Новая вакансия", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 809.156, + "y": 494.5, + "width": 423.328, + "height": 308, + "right": 1232.484, + "bottom": 802.5, + "centerY": 648.5 + }, + "cta": { + "x": 830.156, + "y": 745.5, + "width": 381.328, + "height": 36, + "right": 1211.484, + "bottom": 781.5, + "centerY": 763.5 + } + }, + { + "key": "project:5", + "type": "Новый проект", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 365.828, + "y": 822.5, + "width": 423.328, + "height": 350, + "right": 789.156, + "bottom": 1172.5, + "centerY": 997.5 + }, + "cta": { + "x": 386.828, + "y": 1115.5, + "width": 381.328, + "height": 36, + "right": 768.156, + "bottom": 1151.5, + "centerY": 1133.5 + } + }, + { + "key": "news:6", + "type": "Новости проекта", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": true, + "hasMediaSlot": true, + "rect": { + "x": 809.156, + "y": 822.5, + "width": 423.328, + "height": 350, + "right": 1232.484, + "bottom": 1172.5, + "centerY": 997.5 + }, + "cta": { + "x": 830.156, + "y": 1115.5, + "width": 381.328, + "height": 36, + "right": 1211.484, + "bottom": 1151.5, + "centerY": 1133.5 + } + } + ], + "rows": [ + { + "y": 207, + "keys": ["news:1", "news:2"], + "heights": [267.5, 267.5], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 494.5, + "keys": ["news:3", "vacancy:4"], + "heights": [308, 308], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 822.5, + "keys": ["project:5", "news:6"], + "heights": [350, 350], + "heightDelta": 0, + "ctaYDelta": 0 + } + ], + "filters": [ + { + "label": "все новости", + "count": 12, + "button": { + "x": 365.828, + "y": 83, + "width": 134.438, + "height": 104, + "right": 500.266, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 424.891, + "y": 156, + "width": 16.297, + "height": 18, + "right": 441.188, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "true", + "disabled": false + }, + { + "label": "новости проектов", + "count": 3, + "button": { + "x": 512.266, + "y": 83, + "width": 134.438, + "height": 104, + "right": 646.703, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 575.406, + "y": 156, + "width": 8.156, + "height": 18, + "right": 583.563, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "свежие вакансии", + "count": 3, + "button": { + "x": 658.703, + "y": 83, + "width": 134.438, + "height": 104, + "right": 793.141, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 721.844, + "y": 156, + "width": 8.156, + "height": 18, + "right": 730, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "новости сообщества", + "count": 6, + "button": { + "x": 805.141, + "y": 83, + "width": 134.453, + "height": 104, + "right": 939.594, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 868.281, + "y": 156, + "width": 8.156, + "height": 18, + "right": 876.438, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "новости программ", + "count": 0, + "button": { + "x": 951.594, + "y": 83, + "width": 134.438, + "height": 104, + "right": 1086.031, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 1014.734, + "y": 156, + "width": 8.156, + "height": 18, + "right": 1022.891, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "образование", + "count": 0, + "button": { + "x": 1098.031, + "y": 83, + "width": 134.438, + "height": 104, + "right": 1232.469, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 1161.172, + "y": 156, + "width": 8.156, + "height": 18, + "right": 1169.328, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": true + } + ], + "filterRows": [ + { + "centerY": 165, + "labels": [ + "все новости", + "новости проектов", + "свежие вакансии", + "новости сообщества", + "новости программ", + "образование" + ], + "counts": [12, 3, 3, 6, 0, 0], + "delta": 0 + } + ], + "filterCountCenterYDelta": 0, + "thumbnails": [ + { + "src": "/fixtures/portrait.svg", + "rect": { + "x": 1091.484, + "y": 314, + "width": 120, + "height": 67.5, + "right": 1211.484, + "bottom": 381.5, + "centerY": 347.75 + }, + "objectFit": "cover", + "complete": true, + "naturalWidth": 600, + "naturalHeight": 900 + }, + { + "src": "/fixtures/wide.svg", + "rect": { + "x": 1091.484, + "y": 929.5, + "width": 120, + "height": 67.5, + "right": 1211.484, + "bottom": 997, + "centerY": 963.25 + }, + "objectFit": "cover", + "complete": true, + "naturalWidth": 960, + "naturalHeight": 540 + } + ], + "newsMeta": [ + { + "key": "news:1", + "actions": { + "x": 386.828, + "y": 391.5, + "width": 381.328, + "height": 16, + "right": 768.156, + "bottom": 407.5, + "centerY": 399.5 + }, + "left": { + "x": 386.828, + "y": 391.5, + "width": 69.594, + "height": 16, + "right": 456.422, + "bottom": 407.5, + "centerY": 399.5 + }, + "views": { + "x": 726.563, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 768.156, + "bottom": 407.5, + "centerY": 399.5 + }, + "icons": [ + { + "x": 386.828, + "y": 391.5, + "width": 16, + "height": 16, + "right": 402.828, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 440.422, + "y": 391.5, + "width": 16, + "height": 16, + "right": 456.422, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 726.563, + "y": 391.5, + "width": 16, + "height": 16, + "right": 742.563, + "bottom": 407.5, + "centerY": 399.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 407.828, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 428.422, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 726.563, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 768.156, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 747.563, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 768.156, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": false, + "industryInFooter": false + }, + { + "key": "news:2", + "actions": { + "x": 830.156, + "y": 391.5, + "width": 381.328, + "height": 16, + "right": 1211.484, + "bottom": 407.5, + "centerY": 399.5 + }, + "left": { + "x": 830.156, + "y": 391.5, + "width": 69.594, + "height": 16, + "right": 899.75, + "bottom": 407.5, + "centerY": 399.5 + }, + "views": { + "x": 1169.891, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 1211.484, + "bottom": 407.5, + "centerY": 399.5 + }, + "icons": [ + { + "x": 830.156, + "y": 391.5, + "width": 16, + "height": 16, + "right": 846.156, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 883.75, + "y": 391.5, + "width": 16, + "height": 16, + "right": 899.75, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 1169.891, + "y": 391.5, + "width": 16, + "height": 16, + "right": 1185.891, + "bottom": 407.5, + "centerY": 399.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 851.156, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 871.75, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1169.891, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 1211.484, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1190.891, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 1211.484, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": false, + "industryInFooter": false + }, + { + "key": "news:3", + "actions": { + "x": 386.828, + "y": 719.5, + "width": 381.328, + "height": 16, + "right": 768.156, + "bottom": 735.5, + "centerY": 727.5 + }, + "left": { + "x": 386.828, + "y": 719.5, + "width": 69.594, + "height": 16, + "right": 456.422, + "bottom": 735.5, + "centerY": 727.5 + }, + "views": { + "x": 726.563, + "y": 719.5, + "width": 41.594, + "height": 16, + "right": 768.156, + "bottom": 735.5, + "centerY": 727.5 + }, + "icons": [ + { + "x": 386.828, + "y": 719.5, + "width": 16, + "height": 16, + "right": 402.828, + "bottom": 735.5, + "centerY": 727.5 + }, + { + "x": 440.422, + "y": 719.5, + "width": 16, + "height": 16, + "right": 456.422, + "bottom": 735.5, + "centerY": 727.5 + }, + { + "x": 726.563, + "y": 719.5, + "width": 16, + "height": 16, + "right": 742.563, + "bottom": 735.5, + "centerY": 727.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 407.828, + "y": 719.5, + "width": 20.594, + "height": 16, + "right": 428.422, + "bottom": 735.5, + "centerY": 727.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 726.563, + "y": 719.5, + "width": 41.594, + "height": 16, + "right": 768.156, + "bottom": 735.5, + "centerY": 727.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 747.563, + "y": 719.5, + "width": 20.594, + "height": 16, + "right": 768.156, + "bottom": 735.5, + "centerY": 727.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": true, + "industryInFooter": false + }, + { + "key": "news:6", + "actions": { + "x": 830.156, + "y": 1089.5, + "width": 381.328, + "height": 16, + "right": 1211.484, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "left": { + "x": 830.156, + "y": 1089.5, + "width": 69.594, + "height": 16, + "right": 899.75, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "views": { + "x": 1169.891, + "y": 1089.5, + "width": 41.594, + "height": 16, + "right": 1211.484, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "icons": [ + { + "x": 830.156, + "y": 1089.5, + "width": 16, + "height": 16, + "right": 846.156, + "bottom": 1105.5, + "centerY": 1097.5 + }, + { + "x": 883.75, + "y": 1089.5, + "width": 16, + "height": 16, + "right": 899.75, + "bottom": 1105.5, + "centerY": 1097.5 + }, + { + "x": 1169.891, + "y": 1089.5, + "width": 16, + "height": 16, + "right": 1185.891, + "bottom": 1105.5, + "centerY": 1097.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 851.156, + "y": 1089.5, + "width": 20.594, + "height": 16, + "right": 871.75, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1169.891, + "y": 1089.5, + "width": 41.594, + "height": 16, + "right": 1211.484, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1190.891, + "y": 1089.5, + "width": 20.594, + "height": 16, + "right": 1211.484, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": true, + "industryInFooter": false + } + ] + }, + { + "label": "feed-1280", + "viewport": { + "width": 1280, + "height": 900, + "dpr": 1 + }, + "horizontalOverflowPx": 0, + "overflowCandidates": [ + { + "selector": "html", + "clientWidth": 1280, + "scrollWidth": 1280, + "overflow": 0 + }, + { + "selector": "body", + "clientWidth": 1280, + "scrollWidth": 1280, + "overflow": 0 + }, + { + "selector": "office", + "clientWidth": 1280, + "scrollWidth": 1280, + "overflow": 0 + }, + { + "selector": "office__body", + "clientWidth": 1265, + "scrollWidth": 1265, + "overflow": 0 + }, + { + "selector": "office__inner--content", + "clientWidth": 867, + "scrollWidth": 867, + "overflow": 0 + }, + { + "selector": "page", + "clientWidth": 867, + "scrollWidth": 867, + "overflow": 0 + }, + { + "selector": "page__feed", + "clientWidth": 867, + "scrollWidth": 867, + "overflow": 0 + } + ], + "officeScroll": { + "top": 0, + "height": 900, + "total": 1173 + }, + "grid": { + "columns": "423.328px 423.328px", + "columnGap": "20px", + "rowGap": "20px" + }, + "cards": [ + { + "key": "news:1", + "type": "Новости людей", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 285.828, + "y": 207, + "width": 423.328, + "height": 267.5, + "right": 709.156, + "bottom": 474.5, + "centerY": 340.75 + }, + "cta": { + "x": 306.828, + "y": 417.5, + "width": 381.328, + "height": 36, + "right": 688.156, + "bottom": 453.5, + "centerY": 435.5 + } + }, + { + "key": "news:2", + "type": "Новости людей", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": true, + "hasMediaSlot": true, + "rect": { + "x": 729.156, + "y": 207, + "width": 423.328, + "height": 267.5, + "right": 1152.484, + "bottom": 474.5, + "centerY": 340.75 + }, + "cta": { + "x": 750.156, + "y": 417.5, + "width": 381.328, + "height": 36, + "right": 1131.484, + "bottom": 453.5, + "centerY": 435.5 + } + }, + { + "key": "news:3", + "type": "Новости проекта", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 285.828, + "y": 494.5, + "width": 423.328, + "height": 308, + "right": 709.156, + "bottom": 802.5, + "centerY": 648.5 + }, + "cta": { + "x": 306.828, + "y": 745.5, + "width": 381.328, + "height": 36, + "right": 688.156, + "bottom": 781.5, + "centerY": 763.5 + } + }, + { + "key": "vacancy:4", + "type": "Новая вакансия", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 729.156, + "y": 494.5, + "width": 423.328, + "height": 308, + "right": 1152.484, + "bottom": 802.5, + "centerY": 648.5 + }, + "cta": { + "x": 750.156, + "y": 745.5, + "width": 381.328, + "height": 36, + "right": 1131.484, + "bottom": 781.5, + "centerY": 763.5 + } + }, + { + "key": "project:5", + "type": "Новый проект", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 285.828, + "y": 822.5, + "width": 423.328, + "height": 350, + "right": 709.156, + "bottom": 1172.5, + "centerY": 997.5 + }, + "cta": { + "x": 306.828, + "y": 1115.5, + "width": 381.328, + "height": 36, + "right": 688.156, + "bottom": 1151.5, + "centerY": 1133.5 + } + }, + { + "key": "news:6", + "type": "Новости проекта", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": true, + "hasMediaSlot": true, + "rect": { + "x": 729.156, + "y": 822.5, + "width": 423.328, + "height": 350, + "right": 1152.484, + "bottom": 1172.5, + "centerY": 997.5 + }, + "cta": { + "x": 750.156, + "y": 1115.5, + "width": 381.328, + "height": 36, + "right": 1131.484, + "bottom": 1151.5, + "centerY": 1133.5 + } + } + ], + "rows": [ + { + "y": 207, + "keys": ["news:1", "news:2"], + "heights": [267.5, 267.5], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 494.5, + "keys": ["news:3", "vacancy:4"], + "heights": [308, 308], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 822.5, + "keys": ["project:5", "news:6"], + "heights": [350, 350], + "heightDelta": 0, + "ctaYDelta": 0 + } + ], + "filters": [ + { + "label": "все новости", + "count": 12, + "button": { + "x": 285.828, + "y": 83, + "width": 134.438, + "height": 104, + "right": 420.266, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 344.891, + "y": 156, + "width": 16.297, + "height": 18, + "right": 361.188, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "true", + "disabled": false + }, + { + "label": "новости проектов", + "count": 3, + "button": { + "x": 432.266, + "y": 83, + "width": 134.438, + "height": 104, + "right": 566.703, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 495.406, + "y": 156, + "width": 8.156, + "height": 18, + "right": 503.563, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "свежие вакансии", + "count": 3, + "button": { + "x": 578.703, + "y": 83, + "width": 134.438, + "height": 104, + "right": 713.141, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 641.844, + "y": 156, + "width": 8.156, + "height": 18, + "right": 650, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "новости сообщества", + "count": 6, + "button": { + "x": 725.141, + "y": 83, + "width": 134.453, + "height": 104, + "right": 859.594, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 788.281, + "y": 156, + "width": 8.156, + "height": 18, + "right": 796.438, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "новости программ", + "count": 0, + "button": { + "x": 871.594, + "y": 83, + "width": 134.438, + "height": 104, + "right": 1006.031, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 934.734, + "y": 156, + "width": 8.156, + "height": 18, + "right": 942.891, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "образование", + "count": 0, + "button": { + "x": 1018.031, + "y": 83, + "width": 134.438, + "height": 104, + "right": 1152.469, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 1081.172, + "y": 156, + "width": 8.156, + "height": 18, + "right": 1089.328, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": true + } + ], + "filterRows": [ + { + "centerY": 165, + "labels": [ + "все новости", + "новости проектов", + "свежие вакансии", + "новости сообщества", + "новости программ", + "образование" + ], + "counts": [12, 3, 3, 6, 0, 0], + "delta": 0 + } + ], + "filterCountCenterYDelta": 0, + "thumbnails": [ + { + "src": "/fixtures/portrait.svg", + "rect": { + "x": 1011.484, + "y": 314, + "width": 120, + "height": 67.5, + "right": 1131.484, + "bottom": 381.5, + "centerY": 347.75 + }, + "objectFit": "cover", + "complete": true, + "naturalWidth": 600, + "naturalHeight": 900 + }, + { + "src": "/fixtures/wide.svg", + "rect": { + "x": 1011.484, + "y": 929.5, + "width": 120, + "height": 67.5, + "right": 1131.484, + "bottom": 997, + "centerY": 963.25 + }, + "objectFit": "cover", + "complete": true, + "naturalWidth": 960, + "naturalHeight": 540 + } + ], + "newsMeta": [ + { + "key": "news:1", + "actions": { + "x": 306.828, + "y": 391.5, + "width": 381.328, + "height": 16, + "right": 688.156, + "bottom": 407.5, + "centerY": 399.5 + }, + "left": { + "x": 306.828, + "y": 391.5, + "width": 69.594, + "height": 16, + "right": 376.422, + "bottom": 407.5, + "centerY": 399.5 + }, + "views": { + "x": 646.563, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 688.156, + "bottom": 407.5, + "centerY": 399.5 + }, + "icons": [ + { + "x": 306.828, + "y": 391.5, + "width": 16, + "height": 16, + "right": 322.828, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 360.422, + "y": 391.5, + "width": 16, + "height": 16, + "right": 376.422, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 646.563, + "y": 391.5, + "width": 16, + "height": 16, + "right": 662.563, + "bottom": 407.5, + "centerY": 399.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 327.828, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 348.422, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 646.563, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 688.156, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 667.563, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 688.156, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": false, + "industryInFooter": false + }, + { + "key": "news:2", + "actions": { + "x": 750.156, + "y": 391.5, + "width": 381.328, + "height": 16, + "right": 1131.484, + "bottom": 407.5, + "centerY": 399.5 + }, + "left": { + "x": 750.156, + "y": 391.5, + "width": 69.594, + "height": 16, + "right": 819.75, + "bottom": 407.5, + "centerY": 399.5 + }, + "views": { + "x": 1089.891, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 1131.484, + "bottom": 407.5, + "centerY": 399.5 + }, + "icons": [ + { + "x": 750.156, + "y": 391.5, + "width": 16, + "height": 16, + "right": 766.156, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 803.75, + "y": 391.5, + "width": 16, + "height": 16, + "right": 819.75, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 1089.891, + "y": 391.5, + "width": 16, + "height": 16, + "right": 1105.891, + "bottom": 407.5, + "centerY": 399.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 771.156, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 791.75, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1089.891, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 1131.484, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1110.891, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 1131.484, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": false, + "industryInFooter": false + }, + { + "key": "news:3", + "actions": { + "x": 306.828, + "y": 719.5, + "width": 381.328, + "height": 16, + "right": 688.156, + "bottom": 735.5, + "centerY": 727.5 + }, + "left": { + "x": 306.828, + "y": 719.5, + "width": 69.594, + "height": 16, + "right": 376.422, + "bottom": 735.5, + "centerY": 727.5 + }, + "views": { + "x": 646.563, + "y": 719.5, + "width": 41.594, + "height": 16, + "right": 688.156, + "bottom": 735.5, + "centerY": 727.5 + }, + "icons": [ + { + "x": 306.828, + "y": 719.5, + "width": 16, + "height": 16, + "right": 322.828, + "bottom": 735.5, + "centerY": 727.5 + }, + { + "x": 360.422, + "y": 719.5, + "width": 16, + "height": 16, + "right": 376.422, + "bottom": 735.5, + "centerY": 727.5 + }, + { + "x": 646.563, + "y": 719.5, + "width": 16, + "height": 16, + "right": 662.563, + "bottom": 735.5, + "centerY": 727.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 327.828, + "y": 719.5, + "width": 20.594, + "height": 16, + "right": 348.422, + "bottom": 735.5, + "centerY": 727.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 646.563, + "y": 719.5, + "width": 41.594, + "height": 16, + "right": 688.156, + "bottom": 735.5, + "centerY": 727.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 667.563, + "y": 719.5, + "width": 20.594, + "height": 16, + "right": 688.156, + "bottom": 735.5, + "centerY": 727.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": true, + "industryInFooter": false + }, + { + "key": "news:6", + "actions": { + "x": 750.156, + "y": 1089.5, + "width": 381.328, + "height": 16, + "right": 1131.484, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "left": { + "x": 750.156, + "y": 1089.5, + "width": 69.594, + "height": 16, + "right": 819.75, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "views": { + "x": 1089.891, + "y": 1089.5, + "width": 41.594, + "height": 16, + "right": 1131.484, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "icons": [ + { + "x": 750.156, + "y": 1089.5, + "width": 16, + "height": 16, + "right": 766.156, + "bottom": 1105.5, + "centerY": 1097.5 + }, + { + "x": 803.75, + "y": 1089.5, + "width": 16, + "height": 16, + "right": 819.75, + "bottom": 1105.5, + "centerY": 1097.5 + }, + { + "x": 1089.891, + "y": 1089.5, + "width": 16, + "height": 16, + "right": 1105.891, + "bottom": 1105.5, + "centerY": 1097.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 771.156, + "y": 1089.5, + "width": 20.594, + "height": 16, + "right": 791.75, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1089.891, + "y": 1089.5, + "width": 41.594, + "height": 16, + "right": 1131.484, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1110.891, + "y": 1089.5, + "width": 20.594, + "height": 16, + "right": 1131.484, + "bottom": 1105.5, + "centerY": 1097.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": true, + "industryInFooter": false + } + ] + }, + { + "label": "feed-1024", + "viewport": { + "width": 1024, + "height": 900, + "dpr": 1 + }, + "horizontalOverflowPx": 0, + "overflowCandidates": [ + { + "selector": "html", + "clientWidth": 1024, + "scrollWidth": 1024, + "overflow": 0 + }, + { + "selector": "body", + "clientWidth": 1024, + "scrollWidth": 1024, + "overflow": 0 + }, + { + "selector": "office", + "clientWidth": 1024, + "scrollWidth": 1024, + "overflow": 0 + }, + { + "selector": "office__body", + "clientWidth": 1009, + "scrollWidth": 1009, + "overflow": 0 + }, + { + "selector": "office__inner--content", + "clientWidth": 758, + "scrollWidth": 758, + "overflow": 0 + }, + { + "selector": "page", + "clientWidth": 758, + "scrollWidth": 758, + "overflow": 0 + }, + { + "selector": "page__feed", + "clientWidth": 758, + "scrollWidth": 758, + "overflow": 0 + } + ], + "officeScroll": { + "top": 0, + "height": 900, + "total": 1193 + }, + "grid": { + "columns": "368.75px 368.75px", + "columnGap": "20px", + "rowGap": "20px" + }, + "cards": [ + { + "key": "news:1", + "type": "Новости людей", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 201.5, + "y": 207, + "width": 368.75, + "height": 267.5, + "right": 570.25, + "bottom": 474.5, + "centerY": 340.75 + }, + "cta": { + "x": 222.5, + "y": 417.5, + "width": 326.75, + "height": 36, + "right": 549.25, + "bottom": 453.5, + "centerY": 435.5 + } + }, + { + "key": "news:2", + "type": "Новости людей", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": true, + "hasMediaSlot": true, + "rect": { + "x": 590.25, + "y": 207, + "width": 368.75, + "height": 267.5, + "right": 959, + "bottom": 474.5, + "centerY": 340.75 + }, + "cta": { + "x": 611.25, + "y": 417.5, + "width": 326.75, + "height": 36, + "right": 938, + "bottom": 453.5, + "centerY": 435.5 + } + }, + { + "key": "news:3", + "type": "Новости проекта", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 201.5, + "y": 494.5, + "width": 368.75, + "height": 328, + "right": 570.25, + "bottom": 822.5, + "centerY": 658.5 + }, + "cta": { + "x": 222.5, + "y": 765.5, + "width": 326.75, + "height": 36, + "right": 549.25, + "bottom": 801.5, + "centerY": 783.5 + } + }, + { + "key": "vacancy:4", + "type": "Новая вакансия", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 590.25, + "y": 494.5, + "width": 368.75, + "height": 328, + "right": 959, + "bottom": 822.5, + "centerY": 658.5 + }, + "cta": { + "x": 611.25, + "y": 765.5, + "width": 326.75, + "height": 36, + "right": 938, + "bottom": 801.5, + "centerY": 783.5 + } + }, + { + "key": "project:5", + "type": "Новый проект", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 201.5, + "y": 842.5, + "width": 368.75, + "height": 350, + "right": 570.25, + "bottom": 1192.5, + "centerY": 1017.5 + }, + "cta": { + "x": 222.5, + "y": 1135.5, + "width": 326.75, + "height": 36, + "right": 549.25, + "bottom": 1171.5, + "centerY": 1153.5 + } + }, + { + "key": "news:6", + "type": "Новости проекта", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": true, + "hasMediaSlot": true, + "rect": { + "x": 590.25, + "y": 842.5, + "width": 368.75, + "height": 350, + "right": 959, + "bottom": 1192.5, + "centerY": 1017.5 + }, + "cta": { + "x": 611.25, + "y": 1135.5, + "width": 326.75, + "height": 36, + "right": 938, + "bottom": 1171.5, + "centerY": 1153.5 + } + } + ], + "rows": [ + { + "y": 207, + "keys": ["news:1", "news:2"], + "heights": [267.5, 267.5], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 494.5, + "keys": ["news:3", "vacancy:4"], + "heights": [328, 328], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 842.5, + "keys": ["project:5", "news:6"], + "heights": [350, 350], + "heightDelta": 0, + "ctaYDelta": 0 + } + ], + "filters": [ + { + "label": "все новости", + "count": 12, + "button": { + "x": 201.5, + "y": 83, + "width": 116.25, + "height": 104, + "right": 317.75, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 251.469, + "y": 156, + "width": 16.297, + "height": 18, + "right": 267.766, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "true", + "disabled": false + }, + { + "label": "новости проектов", + "count": 3, + "button": { + "x": 329.75, + "y": 83, + "width": 116.25, + "height": 104, + "right": 446, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 383.797, + "y": 156, + "width": 8.156, + "height": 18, + "right": 391.953, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "свежие вакансии", + "count": 3, + "button": { + "x": 458, + "y": 83, + "width": 116.25, + "height": 104, + "right": 574.25, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 512.047, + "y": 156, + "width": 8.156, + "height": 18, + "right": 520.203, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "новости сообщества", + "count": 6, + "button": { + "x": 586.25, + "y": 83, + "width": 116.25, + "height": 104, + "right": 702.5, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 640.297, + "y": 156, + "width": 8.156, + "height": 18, + "right": 648.453, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "новости программ", + "count": 0, + "button": { + "x": 714.5, + "y": 83, + "width": 116.25, + "height": 104, + "right": 830.75, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 768.547, + "y": 156, + "width": 8.156, + "height": 18, + "right": 776.703, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "образование", + "count": 0, + "button": { + "x": 842.75, + "y": 83, + "width": 116.25, + "height": 104, + "right": 959, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 896.797, + "y": 156, + "width": 8.156, + "height": 18, + "right": 904.953, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": true + } + ], + "filterRows": [ + { + "centerY": 165, + "labels": [ + "все новости", + "новости проектов", + "свежие вакансии", + "новости сообщества", + "новости программ", + "образование" + ], + "counts": [12, 3, 3, 6, 0, 0], + "delta": 0 + } + ], + "filterCountCenterYDelta": 0, + "thumbnails": [ + { + "src": "/fixtures/portrait.svg", + "rect": { + "x": 818, + "y": 314, + "width": 120, + "height": 67.5, + "right": 938, + "bottom": 381.5, + "centerY": 347.75 + }, + "objectFit": "cover", + "complete": true, + "naturalWidth": 600, + "naturalHeight": 900 + }, + { + "src": "/fixtures/wide.svg", + "rect": { + "x": 818, + "y": 949.5, + "width": 120, + "height": 67.5, + "right": 938, + "bottom": 1017, + "centerY": 983.25 + }, + "objectFit": "cover", + "complete": true, + "naturalWidth": 960, + "naturalHeight": 540 + } + ], + "newsMeta": [ + { + "key": "news:1", + "actions": { + "x": 222.5, + "y": 391.5, + "width": 326.75, + "height": 16, + "right": 549.25, + "bottom": 407.5, + "centerY": 399.5 + }, + "left": { + "x": 222.5, + "y": 391.5, + "width": 69.594, + "height": 16, + "right": 292.094, + "bottom": 407.5, + "centerY": 399.5 + }, + "views": { + "x": 507.656, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 549.25, + "bottom": 407.5, + "centerY": 399.5 + }, + "icons": [ + { + "x": 222.5, + "y": 391.5, + "width": 16, + "height": 16, + "right": 238.5, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 276.094, + "y": 391.5, + "width": 16, + "height": 16, + "right": 292.094, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 507.656, + "y": 391.5, + "width": 16, + "height": 16, + "right": 523.656, + "bottom": 407.5, + "centerY": 399.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 243.5, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 264.094, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 507.656, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 549.25, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 528.656, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 549.25, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": false, + "industryInFooter": false + }, + { + "key": "news:2", + "actions": { + "x": 611.25, + "y": 391.5, + "width": 326.75, + "height": 16, + "right": 938, + "bottom": 407.5, + "centerY": 399.5 + }, + "left": { + "x": 611.25, + "y": 391.5, + "width": 69.594, + "height": 16, + "right": 680.844, + "bottom": 407.5, + "centerY": 399.5 + }, + "views": { + "x": 896.406, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 938, + "bottom": 407.5, + "centerY": 399.5 + }, + "icons": [ + { + "x": 611.25, + "y": 391.5, + "width": 16, + "height": 16, + "right": 627.25, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 664.844, + "y": 391.5, + "width": 16, + "height": 16, + "right": 680.844, + "bottom": 407.5, + "centerY": 399.5 + }, + { + "x": 896.406, + "y": 391.5, + "width": 16, + "height": 16, + "right": 912.406, + "bottom": 407.5, + "centerY": 399.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 632.25, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 652.844, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 896.406, + "y": 391.5, + "width": 41.594, + "height": 16, + "right": 938, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 917.406, + "y": 391.5, + "width": 20.594, + "height": 16, + "right": 938, + "bottom": 407.5, + "centerY": 399.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": false, + "industryInFooter": false + }, + { + "key": "news:3", + "actions": { + "x": 222.5, + "y": 739.5, + "width": 326.75, + "height": 16, + "right": 549.25, + "bottom": 755.5, + "centerY": 747.5 + }, + "left": { + "x": 222.5, + "y": 739.5, + "width": 69.594, + "height": 16, + "right": 292.094, + "bottom": 755.5, + "centerY": 747.5 + }, + "views": { + "x": 507.656, + "y": 739.5, + "width": 41.594, + "height": 16, + "right": 549.25, + "bottom": 755.5, + "centerY": 747.5 + }, + "icons": [ + { + "x": 222.5, + "y": 739.5, + "width": 16, + "height": 16, + "right": 238.5, + "bottom": 755.5, + "centerY": 747.5 + }, + { + "x": 276.094, + "y": 739.5, + "width": 16, + "height": 16, + "right": 292.094, + "bottom": 755.5, + "centerY": 747.5 + }, + { + "x": 507.656, + "y": 739.5, + "width": 16, + "height": 16, + "right": 523.656, + "bottom": 755.5, + "centerY": 747.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 243.5, + "y": 739.5, + "width": 20.594, + "height": 16, + "right": 264.094, + "bottom": 755.5, + "centerY": 747.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 507.656, + "y": 739.5, + "width": 41.594, + "height": 16, + "right": 549.25, + "bottom": 755.5, + "centerY": 747.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 528.656, + "y": 739.5, + "width": 20.594, + "height": 16, + "right": 549.25, + "bottom": 755.5, + "centerY": 747.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": true, + "industryInFooter": false + }, + { + "key": "news:6", + "actions": { + "x": 611.25, + "y": 1109.5, + "width": 326.75, + "height": 16, + "right": 938, + "bottom": 1125.5, + "centerY": 1117.5 + }, + "left": { + "x": 611.25, + "y": 1109.5, + "width": 69.594, + "height": 16, + "right": 680.844, + "bottom": 1125.5, + "centerY": 1117.5 + }, + "views": { + "x": 896.406, + "y": 1109.5, + "width": 41.594, + "height": 16, + "right": 938, + "bottom": 1125.5, + "centerY": 1117.5 + }, + "icons": [ + { + "x": 611.25, + "y": 1109.5, + "width": 16, + "height": 16, + "right": 627.25, + "bottom": 1125.5, + "centerY": 1117.5 + }, + { + "x": 664.844, + "y": 1109.5, + "width": 16, + "height": 16, + "right": 680.844, + "bottom": 1125.5, + "centerY": 1117.5 + }, + { + "x": 896.406, + "y": 1109.5, + "width": 16, + "height": 16, + "right": 912.406, + "bottom": 1125.5, + "centerY": 1117.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 632.25, + "y": 1109.5, + "width": 20.594, + "height": 16, + "right": 652.844, + "bottom": 1125.5, + "centerY": 1117.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 896.406, + "y": 1109.5, + "width": 41.594, + "height": 16, + "right": 938, + "bottom": 1125.5, + "centerY": 1117.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 917.406, + "y": 1109.5, + "width": 20.594, + "height": 16, + "right": 938, + "bottom": 1125.5, + "centerY": 1117.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": true, + "industryInFooter": false + } + ] + }, + { + "label": "feed-390", + "viewport": { + "width": 390, + "height": 844, + "dpr": 1 + }, + "horizontalOverflowPx": 0, + "overflowCandidates": [ + { + "selector": "html", + "clientWidth": 390, + "scrollWidth": 390, + "overflow": 0 + }, + { + "selector": "body", + "clientWidth": 390, + "scrollWidth": 390, + "overflow": 0 + }, + { + "selector": "office", + "clientWidth": 390, + "scrollWidth": 390, + "overflow": 0 + }, + { + "selector": "office__body", + "clientWidth": 375, + "scrollWidth": 375, + "overflow": 0 + }, + { + "selector": "office__inner--content", + "clientWidth": 375, + "scrollWidth": 375, + "overflow": 0 + }, + { + "selector": "page", + "clientWidth": 375, + "scrollWidth": 375, + "overflow": 0 + }, + { + "selector": "page__feed", + "clientWidth": 375, + "scrollWidth": 375, + "overflow": 0 + } + ], + "officeScroll": { + "top": 0, + "height": 844, + "total": 2363 + }, + "grid": { + "columns": "375px", + "columnGap": "20px", + "rowGap": "20px" + }, + "cards": [ + { + "key": "news:1", + "type": "Новости людей", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 0, + "y": 515, + "width": 375, + "height": 222, + "right": 375, + "bottom": 737, + "centerY": 626 + }, + "cta": { + "x": 21, + "y": 680, + "width": 333, + "height": 36, + "right": 354, + "bottom": 716, + "centerY": 698 + } + }, + { + "key": "news:2", + "type": "Новости людей", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": true, + "hasMediaSlot": true, + "rect": { + "x": 0, + "y": 757, + "width": 375, + "height": 267.5, + "right": 375, + "bottom": 1024.5, + "centerY": 890.75 + }, + "cta": { + "x": 21, + "y": 967.5, + "width": 333, + "height": 36, + "right": 354, + "bottom": 1003.5, + "centerY": 985.5 + } + }, + { + "key": "news:3", + "type": "Новости проекта", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 0, + "y": 1044.5, + "width": 375, + "height": 306, + "right": 375, + "bottom": 1350.5, + "centerY": 1197.5 + }, + "cta": { + "x": 21, + "y": 1293.5, + "width": 333, + "height": 36, + "right": 354, + "bottom": 1329.5, + "centerY": 1311.5 + } + }, + { + "key": "vacancy:4", + "type": "Новая вакансия", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 0, + "y": 1370.5, + "width": 375, + "height": 326, + "right": 375, + "bottom": 1696.5, + "centerY": 1533.5 + }, + "cta": { + "x": 21, + "y": 1639.5, + "width": 333, + "height": 36, + "right": 354, + "bottom": 1675.5, + "centerY": 1657.5 + } + }, + { + "key": "project:5", + "type": "Новый проект", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 0, + "y": 1716.5, + "width": 375, + "height": 276, + "right": 375, + "bottom": 1992.5, + "centerY": 1854.5 + }, + "cta": { + "x": 21, + "y": 1935.5, + "width": 333, + "height": 36, + "right": 354, + "bottom": 1971.5, + "centerY": 1953.5 + } + }, + { + "key": "news:6", + "type": "Новости проекта", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": true, + "hasMediaSlot": true, + "rect": { + "x": 0, + "y": 2012.5, + "width": 375, + "height": 350, + "right": 375, + "bottom": 2362.5, + "centerY": 2187.5 + }, + "cta": { + "x": 21, + "y": 2305.5, + "width": 333, + "height": 36, + "right": 354, + "bottom": 2341.5, + "centerY": 2323.5 + } + } + ], + "rows": [ + { + "y": 515, + "keys": ["news:1"], + "heights": [222], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 757, + "keys": ["news:2"], + "heights": [267.5], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 1044.5, + "keys": ["news:3"], + "heights": [306], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 1370.5, + "keys": ["vacancy:4"], + "heights": [326], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 1716.5, + "keys": ["project:5"], + "heights": [276], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 2012.5, + "keys": ["news:6"], + "heights": [350], + "heightDelta": 0, + "ctaYDelta": 0 + } + ], + "filters": [ + { + "label": "все новости", + "count": 12, + "button": { + "x": 0, + "y": 65, + "width": 181.5, + "height": 104, + "right": 181.5, + "bottom": 169, + "centerY": 117 + }, + "countRect": { + "x": 82.594, + "y": 138, + "width": 16.297, + "height": 18, + "right": 98.891, + "bottom": 156, + "centerY": 147 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "true", + "disabled": false + }, + { + "label": "новости проектов", + "count": 3, + "button": { + "x": 193.5, + "y": 65, + "width": 181.5, + "height": 104, + "right": 375, + "bottom": 169, + "centerY": 117 + }, + "countRect": { + "x": 280.172, + "y": 138, + "width": 8.156, + "height": 18, + "right": 288.328, + "bottom": 156, + "centerY": 147 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "свежие вакансии", + "count": 3, + "button": { + "x": 0, + "y": 199, + "width": 181.5, + "height": 104, + "right": 181.5, + "bottom": 303, + "centerY": 251 + }, + "countRect": { + "x": 86.672, + "y": 272, + "width": 8.156, + "height": 18, + "right": 94.828, + "bottom": 290, + "centerY": 281 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "новости сообщества", + "count": 6, + "button": { + "x": 193.5, + "y": 199, + "width": 181.5, + "height": 104, + "right": 375, + "bottom": 303, + "centerY": 251 + }, + "countRect": { + "x": 280.172, + "y": 272, + "width": 8.156, + "height": 18, + "right": 288.328, + "bottom": 290, + "centerY": 281 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "новости программ", + "count": 0, + "button": { + "x": 0, + "y": 333, + "width": 181.5, + "height": 104, + "right": 181.5, + "bottom": 437, + "centerY": 385 + }, + "countRect": { + "x": 86.672, + "y": 406, + "width": 8.156, + "height": 18, + "right": 94.828, + "bottom": 424, + "centerY": 415 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "образование", + "count": 0, + "button": { + "x": 193.5, + "y": 333, + "width": 181.5, + "height": 104, + "right": 375, + "bottom": 437, + "centerY": 385 + }, + "countRect": { + "x": 280.172, + "y": 406, + "width": 8.156, + "height": 18, + "right": 288.328, + "bottom": 424, + "centerY": 415 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": true + } + ], + "filterRows": [ + { + "centerY": 147, + "labels": ["все новости", "новости проектов"], + "counts": [12, 3], + "delta": 0 + }, + { + "centerY": 281, + "labels": ["свежие вакансии", "новости сообщества"], + "counts": [3, 6], + "delta": 0 + }, + { + "centerY": 415, + "labels": ["новости программ", "образование"], + "counts": [0, 0], + "delta": 0 + } + ], + "filterCountCenterYDelta": 268, + "thumbnails": [ + { + "src": "/fixtures/portrait.svg", + "rect": { + "x": 234, + "y": 864, + "width": 120, + "height": 67.5, + "right": 354, + "bottom": 931.5, + "centerY": 897.75 + }, + "objectFit": "cover", + "complete": true, + "naturalWidth": 600, + "naturalHeight": 900 + }, + { + "src": "/fixtures/wide.svg", + "rect": { + "x": 234, + "y": 2119.5, + "width": 120, + "height": 67.5, + "right": 354, + "bottom": 2187, + "centerY": 2153.25 + }, + "objectFit": "cover", + "complete": true, + "naturalWidth": 960, + "naturalHeight": 540 + } + ], + "newsMeta": [ + { + "key": "news:1", + "actions": { + "x": 21, + "y": 654, + "width": 333, + "height": 16, + "right": 354, + "bottom": 670, + "centerY": 662 + }, + "left": { + "x": 21, + "y": 654, + "width": 69.594, + "height": 16, + "right": 90.594, + "bottom": 670, + "centerY": 662 + }, + "views": { + "x": 312.406, + "y": 654, + "width": 41.594, + "height": 16, + "right": 354, + "bottom": 670, + "centerY": 662 + }, + "icons": [ + { + "x": 21, + "y": 654, + "width": 16, + "height": 16, + "right": 37, + "bottom": 670, + "centerY": 662 + }, + { + "x": 74.594, + "y": 654, + "width": 16, + "height": 16, + "right": 90.594, + "bottom": 670, + "centerY": 662 + }, + { + "x": 312.406, + "y": 654, + "width": 16, + "height": 16, + "right": 328.406, + "bottom": 670, + "centerY": 662 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 42, + "y": 654, + "width": 20.594, + "height": 16, + "right": 62.594, + "bottom": 670, + "centerY": 662 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 312.406, + "y": 654, + "width": 41.594, + "height": 16, + "right": 354, + "bottom": 670, + "centerY": 662 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 333.406, + "y": 654, + "width": 20.594, + "height": 16, + "right": 354, + "bottom": 670, + "centerY": 662 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": false, + "industryInFooter": false + }, + { + "key": "news:2", + "actions": { + "x": 21, + "y": 941.5, + "width": 333, + "height": 16, + "right": 354, + "bottom": 957.5, + "centerY": 949.5 + }, + "left": { + "x": 21, + "y": 941.5, + "width": 69.594, + "height": 16, + "right": 90.594, + "bottom": 957.5, + "centerY": 949.5 + }, + "views": { + "x": 312.406, + "y": 941.5, + "width": 41.594, + "height": 16, + "right": 354, + "bottom": 957.5, + "centerY": 949.5 + }, + "icons": [ + { + "x": 21, + "y": 941.5, + "width": 16, + "height": 16, + "right": 37, + "bottom": 957.5, + "centerY": 949.5 + }, + { + "x": 74.594, + "y": 941.5, + "width": 16, + "height": 16, + "right": 90.594, + "bottom": 957.5, + "centerY": 949.5 + }, + { + "x": 312.406, + "y": 941.5, + "width": 16, + "height": 16, + "right": 328.406, + "bottom": 957.5, + "centerY": 949.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 42, + "y": 941.5, + "width": 20.594, + "height": 16, + "right": 62.594, + "bottom": 957.5, + "centerY": 949.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 312.406, + "y": 941.5, + "width": 41.594, + "height": 16, + "right": 354, + "bottom": 957.5, + "centerY": 949.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 333.406, + "y": 941.5, + "width": 20.594, + "height": 16, + "right": 354, + "bottom": 957.5, + "centerY": 949.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": false, + "industryInFooter": false + }, + { + "key": "news:3", + "actions": { + "x": 21, + "y": 1267.5, + "width": 333, + "height": 16, + "right": 354, + "bottom": 1283.5, + "centerY": 1275.5 + }, + "left": { + "x": 21, + "y": 1267.5, + "width": 69.594, + "height": 16, + "right": 90.594, + "bottom": 1283.5, + "centerY": 1275.5 + }, + "views": { + "x": 312.406, + "y": 1267.5, + "width": 41.594, + "height": 16, + "right": 354, + "bottom": 1283.5, + "centerY": 1275.5 + }, + "icons": [ + { + "x": 21, + "y": 1267.5, + "width": 16, + "height": 16, + "right": 37, + "bottom": 1283.5, + "centerY": 1275.5 + }, + { + "x": 74.594, + "y": 1267.5, + "width": 16, + "height": 16, + "right": 90.594, + "bottom": 1283.5, + "centerY": 1275.5 + }, + { + "x": 312.406, + "y": 1267.5, + "width": 16, + "height": 16, + "right": 328.406, + "bottom": 1283.5, + "centerY": 1275.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 42, + "y": 1267.5, + "width": 20.594, + "height": 16, + "right": 62.594, + "bottom": 1283.5, + "centerY": 1275.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 312.406, + "y": 1267.5, + "width": 41.594, + "height": 16, + "right": 354, + "bottom": 1283.5, + "centerY": 1275.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 333.406, + "y": 1267.5, + "width": 20.594, + "height": 16, + "right": 354, + "bottom": 1283.5, + "centerY": 1275.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": true, + "industryInFooter": false + }, + { + "key": "news:6", + "actions": { + "x": 21, + "y": 2279.5, + "width": 333, + "height": 16, + "right": 354, + "bottom": 2295.5, + "centerY": 2287.5 + }, + "left": { + "x": 21, + "y": 2279.5, + "width": 69.594, + "height": 16, + "right": 90.594, + "bottom": 2295.5, + "centerY": 2287.5 + }, + "views": { + "x": 312.406, + "y": 2279.5, + "width": 41.594, + "height": 16, + "right": 354, + "bottom": 2295.5, + "centerY": 2287.5 + }, + "icons": [ + { + "x": 21, + "y": 2279.5, + "width": 16, + "height": 16, + "right": 37, + "bottom": 2295.5, + "centerY": 2287.5 + }, + { + "x": 74.594, + "y": 2279.5, + "width": 16, + "height": 16, + "right": 90.594, + "bottom": 2295.5, + "centerY": 2287.5 + }, + { + "x": 312.406, + "y": 2279.5, + "width": 16, + "height": 16, + "right": 328.406, + "bottom": 2295.5, + "centerY": 2287.5 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 42, + "y": 2279.5, + "width": 20.594, + "height": 16, + "right": 62.594, + "bottom": 2295.5, + "centerY": 2287.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 312.406, + "y": 2279.5, + "width": 41.594, + "height": 16, + "right": 354, + "bottom": 2295.5, + "centerY": 2287.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 333.406, + "y": 2279.5, + "width": 20.594, + "height": 16, + "right": 354, + "bottom": 2295.5, + "centerY": 2287.5 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": true, + "industryInFooter": false + } + ] + }, + { + "label": "extreme-1440", + "viewport": { + "width": 1440, + "height": 900, + "dpr": 1 + }, + "horizontalOverflowPx": 0, + "overflowCandidates": [ + { + "selector": "html", + "clientWidth": 1440, + "scrollWidth": 1440, + "overflow": 0 + }, + { + "selector": "body", + "clientWidth": 1440, + "scrollWidth": 1440, + "overflow": 0 + }, + { + "selector": "office", + "clientWidth": 1440, + "scrollWidth": 1440, + "overflow": 0 + }, + { + "selector": "office__body", + "clientWidth": 1425, + "scrollWidth": 1425, + "overflow": 0 + }, + { + "selector": "office__inner--content", + "clientWidth": 867, + "scrollWidth": 867, + "overflow": 0 + }, + { + "selector": "page", + "clientWidth": 867, + "scrollWidth": 867, + "overflow": 0 + }, + { + "selector": "page__feed", + "clientWidth": 867, + "scrollWidth": 867, + "overflow": 0 + } + ], + "officeScroll": { + "top": 0, + "height": 900, + "total": 1273 + }, + "grid": { + "columns": "423.328px 423.328px", + "columnGap": "20px", + "rowGap": "20px" + }, + "cards": [ + { + "key": "news:1", + "type": "Новости людей", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 365.828, + "y": 207, + "width": 423.328, + "height": 328, + "right": 789.156, + "bottom": 535, + "centerY": 371 + }, + "cta": { + "x": 386.828, + "y": 478, + "width": 381.328, + "height": 36, + "right": 768.156, + "bottom": 514, + "centerY": 496 + } + }, + { + "key": "news:2", + "type": "Новости людей", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": true, + "hasMediaSlot": true, + "rect": { + "x": 809.156, + "y": 207, + "width": 423.328, + "height": 328, + "right": 1232.484, + "bottom": 535, + "centerY": 371 + }, + "cta": { + "x": 830.156, + "y": 478, + "width": 381.328, + "height": 36, + "right": 1211.484, + "bottom": 514, + "centerY": 496 + } + }, + { + "key": "news:3", + "type": "Новости проекта", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 365.828, + "y": 555, + "width": 423.328, + "height": 348, + "right": 789.156, + "bottom": 903, + "centerY": 729 + }, + "cta": { + "x": 386.828, + "y": 846, + "width": 381.328, + "height": 36, + "right": 768.156, + "bottom": 882, + "centerY": 864 + } + }, + { + "key": "vacancy:4", + "type": "Новая вакансия", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 809.156, + "y": 555, + "width": 423.328, + "height": 348, + "right": 1232.484, + "bottom": 903, + "centerY": 729 + }, + "cta": { + "x": 830.156, + "y": 846, + "width": 381.328, + "height": 36, + "right": 1211.484, + "bottom": 882, + "centerY": 864 + } + }, + { + "key": "project:5", + "type": "Новый проект", + "date": "23.09.2026", + "hasMore": false, + "hasThumbnail": false, + "hasMediaSlot": false, + "rect": { + "x": 365.828, + "y": 923, + "width": 423.328, + "height": 350, + "right": 789.156, + "bottom": 1273, + "centerY": 1098 + }, + "cta": { + "x": 386.828, + "y": 1216, + "width": 381.328, + "height": 36, + "right": 768.156, + "bottom": 1252, + "centerY": 1234 + } + }, + { + "key": "news:6", + "type": "Новости проекта", + "date": "23.09.2026", + "hasMore": true, + "hasThumbnail": true, + "hasMediaSlot": true, + "rect": { + "x": 809.156, + "y": 923, + "width": 423.328, + "height": 350, + "right": 1232.484, + "bottom": 1273, + "centerY": 1098 + }, + "cta": { + "x": 830.156, + "y": 1216, + "width": 381.328, + "height": 36, + "right": 1211.484, + "bottom": 1252, + "centerY": 1234 + } + } + ], + "rows": [ + { + "y": 207, + "keys": ["news:1", "news:2"], + "heights": [328, 328], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 555, + "keys": ["news:3", "vacancy:4"], + "heights": [348, 348], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 923, + "keys": ["project:5", "news:6"], + "heights": [350, 350], + "heightDelta": 0, + "ctaYDelta": 0 + } + ], + "filters": [ + { + "label": "все новости", + "count": 12, + "button": { + "x": 365.828, + "y": 83, + "width": 134.438, + "height": 104, + "right": 500.266, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 424.891, + "y": 156, + "width": 16.297, + "height": 18, + "right": 441.188, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "true", + "disabled": false + }, + { + "label": "новости проектов", + "count": 3, + "button": { + "x": 512.266, + "y": 83, + "width": 134.438, + "height": 104, + "right": 646.703, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 575.406, + "y": 156, + "width": 8.156, + "height": 18, + "right": 583.563, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "свежие вакансии", + "count": 3, + "button": { + "x": 658.703, + "y": 83, + "width": 134.438, + "height": 104, + "right": 793.141, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 721.844, + "y": 156, + "width": 8.156, + "height": 18, + "right": 730, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "новости сообщества", + "count": 6, + "button": { + "x": 805.141, + "y": 83, + "width": 134.453, + "height": 104, + "right": 939.594, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 868.281, + "y": 156, + "width": 8.156, + "height": 18, + "right": 876.438, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "новости программ", + "count": 0, + "button": { + "x": 951.594, + "y": 83, + "width": 134.438, + "height": 104, + "right": 1086.031, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 1014.734, + "y": 156, + "width": 8.156, + "height": 18, + "right": 1022.891, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": false + }, + { + "label": "образование", + "count": 0, + "button": { + "x": 1098.031, + "y": 83, + "width": 134.438, + "height": 104, + "right": 1232.469, + "bottom": 187, + "centerY": 135 + }, + "countRect": { + "x": 1161.172, + "y": 156, + "width": 8.156, + "height": 18, + "right": 1169.328, + "bottom": 174, + "centerY": 165 + }, + "fontSize": "14px", + "fontWeight": "600", + "lineHeight": "18px", + "fontVariantNumeric": "tabular-nums", + "ariaPressed": "false", + "disabled": true + } + ], + "filterRows": [ + { + "centerY": 165, + "labels": [ + "все новости", + "новости проектов", + "свежие вакансии", + "новости сообщества", + "новости программ", + "образование" + ], + "counts": [12, 3, 3, 6, 0, 0], + "delta": 0 + } + ], + "filterCountCenterYDelta": 0, + "thumbnails": [ + { + "src": "/fixtures/portrait.svg", + "rect": { + "x": 1091.484, + "y": 314, + "width": 120, + "height": 67.5, + "right": 1211.484, + "bottom": 381.5, + "centerY": 347.75 + }, + "objectFit": "cover", + "complete": true, + "naturalWidth": 600, + "naturalHeight": 900 + }, + { + "src": "/fixtures/wide.svg", + "rect": { + "x": 1091.484, + "y": 1030, + "width": 120, + "height": 67.5, + "right": 1211.484, + "bottom": 1097.5, + "centerY": 1063.75 + }, + "objectFit": "cover", + "complete": true, + "naturalWidth": 960, + "naturalHeight": 540 + } + ], + "newsMeta": [ + { + "key": "news:1", + "actions": { + "x": 386.828, + "y": 452, + "width": 381.328, + "height": 16, + "right": 768.156, + "bottom": 468, + "centerY": 460 + }, + "left": { + "x": 386.828, + "y": 452, + "width": 69.594, + "height": 16, + "right": 456.422, + "bottom": 468, + "centerY": 460 + }, + "views": { + "x": 726.563, + "y": 452, + "width": 41.594, + "height": 16, + "right": 768.156, + "bottom": 468, + "centerY": 460 + }, + "icons": [ + { + "x": 386.828, + "y": 452, + "width": 16, + "height": 16, + "right": 402.828, + "bottom": 468, + "centerY": 460 + }, + { + "x": 440.422, + "y": 452, + "width": 16, + "height": 16, + "right": 456.422, + "bottom": 468, + "centerY": 460 + }, + { + "x": 726.563, + "y": 452, + "width": 16, + "height": 16, + "right": 742.563, + "bottom": 468, + "centerY": 460 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 407.828, + "y": 452, + "width": 20.594, + "height": 16, + "right": 428.422, + "bottom": 468, + "centerY": 460 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 726.563, + "y": 452, + "width": 41.594, + "height": 16, + "right": 768.156, + "bottom": 468, + "centerY": 460 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 747.563, + "y": 452, + "width": 20.594, + "height": 16, + "right": 768.156, + "bottom": 468, + "centerY": 460 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": false, + "industryInFooter": false + }, + { + "key": "news:2", + "actions": { + "x": 830.156, + "y": 452, + "width": 381.328, + "height": 16, + "right": 1211.484, + "bottom": 468, + "centerY": 460 + }, + "left": { + "x": 830.156, + "y": 452, + "width": 69.594, + "height": 16, + "right": 899.75, + "bottom": 468, + "centerY": 460 + }, + "views": { + "x": 1169.891, + "y": 452, + "width": 41.594, + "height": 16, + "right": 1211.484, + "bottom": 468, + "centerY": 460 + }, + "icons": [ + { + "x": 830.156, + "y": 452, + "width": 16, + "height": 16, + "right": 846.156, + "bottom": 468, + "centerY": 460 + }, + { + "x": 883.75, + "y": 452, + "width": 16, + "height": 16, + "right": 899.75, + "bottom": 468, + "centerY": 460 + }, + { + "x": 1169.891, + "y": 452, + "width": 16, + "height": 16, + "right": 1185.891, + "bottom": 468, + "centerY": 460 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 851.156, + "y": 452, + "width": 20.594, + "height": 16, + "right": 871.75, + "bottom": 468, + "centerY": 460 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1169.891, + "y": 452, + "width": 41.594, + "height": 16, + "right": 1211.484, + "bottom": 468, + "centerY": 460 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1190.891, + "y": 452, + "width": 20.594, + "height": 16, + "right": 1211.484, + "bottom": 468, + "centerY": 460 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": false, + "industryInFooter": false + }, + { + "key": "news:3", + "actions": { + "x": 386.828, + "y": 820, + "width": 381.328, + "height": 16, + "right": 768.156, + "bottom": 836, + "centerY": 828 + }, + "left": { + "x": 386.828, + "y": 820, + "width": 69.594, + "height": 16, + "right": 456.422, + "bottom": 836, + "centerY": 828 + }, + "views": { + "x": 726.563, + "y": 820, + "width": 41.594, + "height": 16, + "right": 768.156, + "bottom": 836, + "centerY": 828 + }, + "icons": [ + { + "x": 386.828, + "y": 820, + "width": 16, + "height": 16, + "right": 402.828, + "bottom": 836, + "centerY": 828 + }, + { + "x": 440.422, + "y": 820, + "width": 16, + "height": 16, + "right": 456.422, + "bottom": 836, + "centerY": 828 + }, + { + "x": 726.563, + "y": 820, + "width": 16, + "height": 16, + "right": 742.563, + "bottom": 836, + "centerY": 828 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 407.828, + "y": 820, + "width": 20.594, + "height": 16, + "right": 428.422, + "bottom": 836, + "centerY": 828 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 726.563, + "y": 820, + "width": 41.594, + "height": 16, + "right": 768.156, + "bottom": 836, + "centerY": 828 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 747.563, + "y": 820, + "width": 20.594, + "height": 16, + "right": 768.156, + "bottom": 836, + "centerY": 828 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": true, + "industryInFooter": false + }, + { + "key": "news:6", + "actions": { + "x": 830.156, + "y": 1190, + "width": 381.328, + "height": 16, + "right": 1211.484, + "bottom": 1206, + "centerY": 1198 + }, + "left": { + "x": 830.156, + "y": 1190, + "width": 69.594, + "height": 16, + "right": 899.75, + "bottom": 1206, + "centerY": 1198 + }, + "views": { + "x": 1169.891, + "y": 1190, + "width": 41.594, + "height": 16, + "right": 1211.484, + "bottom": 1206, + "centerY": 1198 + }, + "icons": [ + { + "x": 830.156, + "y": 1190, + "width": 16, + "height": 16, + "right": 846.156, + "bottom": 1206, + "centerY": 1198 + }, + { + "x": 883.75, + "y": 1190, + "width": 16, + "height": 16, + "right": 899.75, + "bottom": 1206, + "centerY": 1198 + }, + { + "x": 1169.891, + "y": 1190, + "width": 16, + "height": 16, + "right": 1185.891, + "bottom": 1206, + "centerY": 1198 + } + ], + "numbers": [ + { + "value": "234", + "rect": { + "x": 851.156, + "y": 1190, + "width": 20.594, + "height": 16, + "right": 871.75, + "bottom": 1206, + "centerY": 1198 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1169.891, + "y": 1190, + "width": 41.594, + "height": 16, + "right": 1211.484, + "bottom": 1206, + "centerY": 1198 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + }, + { + "value": "234", + "rect": { + "x": 1190.891, + "y": 1190, + "width": 20.594, + "height": 16, + "right": 1211.484, + "bottom": 1206, + "centerY": 1198 + }, + "fontSize": "12px", + "lineHeight": "16px", + "fontVariantNumeric": "tabular-nums" + } + ], + "centerlineDelta": 0, + "leftInset": 21, + "rightInset": 21, + "industryInTopMeta": true, + "industryInFooter": false + } + ] + } + ], + "pagination": { + "beforeCount": 6, + "afterCount": 12, + "beforeKeys": ["news:1", "news:2", "news:3", "vacancy:4", "project:5", "news:6"], + "afterKeys": [ + "news:1", + "news:2", + "news:3", + "vacancy:4", + "project:5", + "news:6", + "vacancy:7", + "project:8", + "news:9", + "news:10", + "vacancy:11", + "project:12" + ], + "prefixPreserved": true, + "uniqueKeys": true, + "fetchCalls": [ + { + "offset": 6, + "limit": 6, + "type": "vacancy|project|news" + } + ], + "horizontalOverflowPx": 0, + "rowsAfterAppend": [ + { + "y": -1048, + "keys": ["news:1", "news:2"], + "heights": [267.5, 267.5], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": -760.5, + "keys": ["news:3", "vacancy:4"], + "heights": [308, 308], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": -432.5, + "keys": ["project:5", "news:6"], + "heights": [350, 350], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": -62.5, + "keys": ["vacancy:7", "project:8"], + "heights": [308, 308], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 265.5, + "keys": ["news:9", "news:10"], + "heights": [306, 306], + "heightDelta": 0, + "ctaYDelta": 0 + }, + { + "y": 591.5, + "keys": ["vacancy:11", "project:12"], + "heights": [308, 308], + "heightDelta": 0, + "ctaYDelta": 0 + } + ] + }, + "filters": { + "countsBefore": { + "все новости": 12, + "новости проектов": 3, + "свежие вакансии": 3, + "новости сообщества": 6, + "новости программ": 0, + "образование": 0 + }, + "countsAfter": { + "все новости": 12, + "новости проектов": 3, + "свежие вакансии": 3, + "новости сообщества": 6, + "новости программ": 0, + "образование": 0 + }, + "activeAfter": "новости сообщества", + "resultKeysAfter": ["news:1", "news:2", "news:3", "news:6", "news:9", "news:10"], + "fetchCalls": [ + { + "offset": 0, + "limit": 6, + "type": "news" + } + ], + "desktopCountCenterYDeltaPx": 0, + "mobileClosedRows": [ + { + "centerY": 147, + "labels": ["все новости", "новости проектов"], + "counts": [12, 3], + "delta": 0 + }, + { + "centerY": 281, + "labels": ["свежие вакансии", "новости сообщества"], + "counts": [3, 6], + "delta": 0 + }, + { + "centerY": 415, + "labels": ["новости программ", "образование"], + "counts": [0, 0], + "delta": 0 + } + ], + "mobileOpenRows": [ + { + "centerY": 147, + "labels": ["все новости", "новости проектов"], + "counts": [12, 3], + "delta": 0 + }, + { + "centerY": 281, + "labels": ["свежие вакансии", "новости сообщества"], + "counts": [3, 6], + "delta": 0 + }, + { + "centerY": 415, + "labels": ["новости программ", "образование"], + "counts": [0, 0], + "delta": 0 + }, + { + "centerY": 595.391, + "labels": ["все новости", "новости проектов"], + "counts": [12, 3], + "delta": 0 + }, + { + "centerY": 729.391, + "labels": ["свежие вакансии", "новости сообщества"], + "counts": [3, 6], + "delta": 0 + }, + { + "centerY": 863.391, + "labels": ["новости программ", "образование"], + "counts": [0, 0], + "delta": 0 + } + ], + "localTestRouterAddress": "http://127.0.0.1:4347/office/feed" + }, + "modal": [ + { + "label": "attachment-1440", + "viewport": { + "width": 1440, + "height": 900, + "dpr": 1 + }, + "body": { + "x": 280, + "y": 45, + "width": 880, + "height": 810, + "right": 1160, + "bottom": 855, + "centerY": 450 + }, + "header": { + "x": 280, + "y": 45, + "width": 880, + "height": 61, + "right": 1160, + "bottom": 106, + "centerY": 75.5 + }, + "close": { + "x": 1104, + "y": 57, + "width": 36, + "height": 36, + "right": 1140, + "bottom": 93, + "centerY": 75 + }, + "scroll": { + "top": 0, + "height": 749, + "total": 908, + "rect": { + "x": 280, + "y": 106, + "width": 880, + "height": 749, + "right": 1160, + "bottom": 855, + "centerY": 480.5 + } + }, + "image": { + "src": "/fixtures/wide.svg", + "alt": "wide.svg", + "naturalWidth": 960, + "naturalHeight": 540, + "box": { + "x": 300, + "y": 383.125, + "width": 825, + "height": 464.063, + "right": 1125, + "bottom": 847.188, + "centerY": 615.156 + }, + "paintedWidth": 825, + "paintedHeight": 464.063, + "objectFit": "contain", + "complete": true, + "aspectRatioDelta": 0 + }, + "carouselIndex": null, + "focus": { + "tag": "BUTTON", + "ariaLabel": "Закрыть новость", + "className": "feed-news-modal__close" + }, + "actions": { + "likeText": "234", + "likePressed": "true", + "viewsLabel": "Просмотров: 234", + "iconRects": [ + { + "x": 304, + "y": 916.766, + "width": 16, + "height": 16, + "right": 320, + "bottom": 932.766, + "centerY": 924.766 + }, + { + "x": 367.375, + "y": 916.766, + "width": 16, + "height": 16, + "right": 383.375, + "bottom": 932.766, + "centerY": 924.766 + }, + { + "x": 1081.625, + "y": 916.766, + "width": 16, + "height": 16, + "right": 1097.625, + "bottom": 932.766, + "centerY": 924.766 + } + ], + "centerlineDelta": 0.007 + }, + "files": [ + { + "name": "Описание проекта.txt", + "href": "/fixtures/notes.txt", + "download": "Описание проекта.txt" + } + ], + "fixtureCalls": { + "fetch": [ + { + "offset": 6, + "limit": 6, + "type": "vacancy|project|news" + } + ], + "like": [], + "clipboard": [] + } + }, + { + "label": "portrait-1440", + "viewport": { + "width": 1440, + "height": 900, + "dpr": 1 + }, + "body": { + "x": 280, + "y": 45, + "width": 880, + "height": 810, + "right": 1160, + "bottom": 855, + "centerY": 450 + }, + "header": { + "x": 280, + "y": 45, + "width": 880, + "height": 61, + "right": 1160, + "bottom": 106, + "centerY": 75.5 + }, + "close": { + "x": 1104, + "y": 57, + "width": 36, + "height": 36, + "right": 1140, + "bottom": 93, + "centerY": 75 + }, + "scroll": { + "top": 0, + "height": 749, + "total": 857, + "rect": { + "x": 280, + "y": 106, + "width": 880, + "height": 749, + "right": 1160, + "bottom": 855, + "centerY": 480.5 + } + }, + "image": { + "src": "/fixtures/portrait.svg", + "alt": "portrait.svg", + "naturalWidth": 600, + "naturalHeight": 900, + "box": { + "x": 300, + "y": 226.391, + "width": 825, + "height": 612, + "right": 1125, + "bottom": 838.391, + "centerY": 532.391 + }, + "paintedWidth": 408, + "paintedHeight": 612, + "objectFit": "contain", + "complete": true, + "aspectRatioDelta": 0 + }, + "carouselIndex": "1 / 5", + "focus": { + "tag": "BUTTON", + "ariaLabel": "Закрыть новость", + "className": "feed-news-modal__close" + }, + "actions": { + "likeText": "234", + "likePressed": "true", + "viewsLabel": "Просмотров: 234", + "iconRects": [ + { + "x": 304, + "y": 865.578, + "width": 16, + "height": 16, + "right": 320, + "bottom": 881.578, + "centerY": 873.578 + }, + { + "x": 367.375, + "y": 865.578, + "width": 16, + "height": 16, + "right": 383.375, + "bottom": 881.578, + "centerY": 873.578 + }, + { + "x": 1081.625, + "y": 865.578, + "width": 16, + "height": 16, + "right": 1097.625, + "bottom": 881.578, + "centerY": 873.578 + } + ], + "centerlineDelta": 0.008 + }, + "files": [], + "fixtureCalls": { + "fetch": [], + "like": [], + "clipboard": [] + } + }, + { + "label": "square-1440", + "viewport": { + "width": 1440, + "height": 900, + "dpr": 1 + }, + "body": { + "x": 280, + "y": 45, + "width": 880, + "height": 810, + "right": 1160, + "bottom": 855, + "centerY": 450 + }, + "header": { + "x": 280, + "y": 45, + "width": 880, + "height": 61, + "right": 1160, + "bottom": 106, + "centerY": 75.5 + }, + "close": { + "x": 1104, + "y": 57, + "width": 36, + "height": 36, + "right": 1140, + "bottom": 93, + "centerY": 75 + }, + "scroll": { + "top": 0, + "height": 749, + "total": 857, + "rect": { + "x": 280, + "y": 106, + "width": 880, + "height": 749, + "right": 1160, + "bottom": 855, + "centerY": 480.5 + } + }, + "image": { + "src": "/fixtures/square.svg", + "alt": "square.svg", + "naturalWidth": 600, + "naturalHeight": 600, + "box": { + "x": 300, + "y": 226.391, + "width": 825, + "height": 612, + "right": 1125, + "bottom": 838.391, + "centerY": 532.391 + }, + "paintedWidth": 612, + "paintedHeight": 612, + "objectFit": "contain", + "complete": true, + "aspectRatioDelta": 0 + }, + "carouselIndex": "2 / 5", + "focus": { + "tag": "BUTTON", + "ariaLabel": "Следующее изображение", + "className": "carousel__btn carousel__btn--next ng-star-inserted" + }, + "actions": { + "likeText": "234", + "likePressed": "true", + "viewsLabel": "Просмотров: 234", + "iconRects": [ + { + "x": 304, + "y": 865.578, + "width": 16, + "height": 16, + "right": 320, + "bottom": 881.578, + "centerY": 873.578 + }, + { + "x": 367.375, + "y": 865.578, + "width": 16, + "height": 16, + "right": 383.375, + "bottom": 881.578, + "centerY": 873.578 + }, + { + "x": 1081.625, + "y": 865.578, + "width": 16, + "height": 16, + "right": 1097.625, + "bottom": 881.578, + "centerY": 873.578 + } + ], + "centerlineDelta": 0.008 + }, + "files": [], + "fixtureCalls": { + "fetch": [], + "like": [], + "clipboard": [] + } + }, + { + "label": "landscape-1440", + "viewport": { + "width": 1440, + "height": 900, + "dpr": 1 + }, + "body": { + "x": 280, + "y": 45, + "width": 880, + "height": 810, + "right": 1160, + "bottom": 855, + "centerY": 450 + }, + "header": { + "x": 280, + "y": 45, + "width": 880, + "height": 61, + "right": 1160, + "bottom": 106, + "centerY": 75.5 + }, + "close": { + "x": 1104, + "y": 57, + "width": 36, + "height": 36, + "right": 1140, + "bottom": 93, + "centerY": 75 + }, + "scroll": { + "top": 0, + "height": 749, + "total": 857, + "rect": { + "x": 280, + "y": 106, + "width": 880, + "height": 749, + "right": 1160, + "bottom": 855, + "centerY": 480.5 + } + }, + "image": { + "src": "/fixtures/landscape.svg", + "alt": "landscape.svg", + "naturalWidth": 800, + "naturalHeight": 600, + "box": { + "x": 300, + "y": 226.391, + "width": 825, + "height": 612, + "right": 1125, + "bottom": 838.391, + "centerY": 532.391 + }, + "paintedWidth": 816, + "paintedHeight": 612, + "objectFit": "contain", + "complete": true, + "aspectRatioDelta": 0 + }, + "carouselIndex": "3 / 5", + "focus": { + "tag": "BUTTON", + "ariaLabel": "Следующее изображение", + "className": "carousel__btn carousel__btn--next ng-star-inserted" + }, + "actions": { + "likeText": "234", + "likePressed": "true", + "viewsLabel": "Просмотров: 234", + "iconRects": [ + { + "x": 304, + "y": 865.578, + "width": 16, + "height": 16, + "right": 320, + "bottom": 881.578, + "centerY": 873.578 + }, + { + "x": 367.375, + "y": 865.578, + "width": 16, + "height": 16, + "right": 383.375, + "bottom": 881.578, + "centerY": 873.578 + }, + { + "x": 1081.625, + "y": 865.578, + "width": 16, + "height": 16, + "right": 1097.625, + "bottom": 881.578, + "centerY": 873.578 + } + ], + "centerlineDelta": 0.008 + }, + "files": [], + "fixtureCalls": { + "fetch": [], + "like": [], + "clipboard": [] + } + }, + { + "label": "wide-1440", + "viewport": { + "width": 1440, + "height": 900, + "dpr": 1 + }, + "body": { + "x": 280, + "y": 60.664, + "width": 880, + "height": 778.672, + "right": 1160, + "bottom": 839.336, + "centerY": 450 + }, + "header": { + "x": 280, + "y": 60.664, + "width": 880, + "height": 61, + "right": 1160, + "bottom": 121.664, + "centerY": 91.164 + }, + "close": { + "x": 1104, + "y": 72.664, + "width": 36, + "height": 36, + "right": 1140, + "bottom": 108.664, + "centerY": 90.664 + }, + "scroll": { + "top": 0, + "height": 718, + "total": 718, + "rect": { + "x": 280, + "y": 121.664, + "width": 880, + "height": 717.672, + "right": 1160, + "bottom": 839.336, + "centerY": 480.5 + } + }, + "image": { + "src": "/fixtures/wide.svg", + "alt": "wide.svg", + "naturalWidth": 960, + "naturalHeight": 540, + "box": { + "x": 300, + "y": 242.055, + "width": 840, + "height": 472.5, + "right": 1140, + "bottom": 714.555, + "centerY": 478.305 + }, + "paintedWidth": 840, + "paintedHeight": 472.5, + "objectFit": "contain", + "complete": true, + "aspectRatioDelta": 0 + }, + "carouselIndex": "4 / 5", + "focus": { + "tag": "BUTTON", + "ariaLabel": "Следующее изображение", + "className": "carousel__btn carousel__btn--next ng-star-inserted" + }, + "actions": { + "likeText": "234", + "likePressed": "true", + "viewsLabel": "Просмотров: 234", + "iconRects": [ + { + "x": 304, + "y": 741.742, + "width": 16, + "height": 16, + "right": 320, + "bottom": 757.742, + "centerY": 749.742 + }, + { + "x": 367.375, + "y": 741.742, + "width": 16, + "height": 16, + "right": 383.375, + "bottom": 757.742, + "centerY": 749.742 + }, + { + "x": 1096.625, + "y": 741.742, + "width": 16, + "height": 16, + "right": 1112.625, + "bottom": 757.742, + "centerY": 749.742 + } + ], + "centerlineDelta": 0.008 + }, + "files": [], + "fixtureCalls": { + "fetch": [], + "like": [], + "clipboard": [] + } + }, + { + "label": "panorama-1440", + "viewport": { + "width": 1440, + "height": 900, + "dpr": 1 + }, + "body": { + "x": 280, + "y": 212.914, + "width": 880, + "height": 474.172, + "right": 1160, + "bottom": 687.086, + "centerY": 450 + }, + "header": { + "x": 280, + "y": 212.914, + "width": 880, + "height": 61, + "right": 1160, + "bottom": 273.914, + "centerY": 243.414 + }, + "close": { + "x": 1104, + "y": 224.914, + "width": 36, + "height": 36, + "right": 1140, + "bottom": 260.914, + "centerY": 242.914 + }, + "scroll": { + "top": 0, + "height": 413, + "total": 413, + "rect": { + "x": 280, + "y": 273.914, + "width": 880, + "height": 413.172, + "right": 1160, + "bottom": 687.086, + "centerY": 480.5 + } + }, + "image": { + "src": "/fixtures/panorama.svg", + "alt": "panorama.svg", + "naturalWidth": 1500, + "naturalHeight": 300, + "box": { + "x": 300, + "y": 394.305, + "width": 840, + "height": 168, + "right": 1140, + "bottom": 562.305, + "centerY": 478.305 + }, + "paintedWidth": 840, + "paintedHeight": 168, + "objectFit": "contain", + "complete": true, + "aspectRatioDelta": 0 + }, + "carouselIndex": "5 / 5", + "focus": { + "tag": "BUTTON", + "ariaLabel": "Следующее изображение", + "className": "carousel__btn carousel__btn--next ng-star-inserted" + }, + "actions": { + "likeText": "234", + "likePressed": "true", + "viewsLabel": "Просмотров: 234", + "iconRects": [ + { + "x": 304, + "y": 589.492, + "width": 16, + "height": 16, + "right": 320, + "bottom": 605.492, + "centerY": 597.492 + }, + { + "x": 367.375, + "y": 589.492, + "width": 16, + "height": 16, + "right": 383.375, + "bottom": 605.492, + "centerY": 597.492 + }, + { + "x": 1096.625, + "y": 589.492, + "width": 16, + "height": 16, + "right": 1112.625, + "bottom": 605.492, + "centerY": 597.492 + } + ], + "centerlineDelta": 0.008 + }, + "files": [], + "fixtureCalls": { + "fetch": [], + "like": [], + "clipboard": [] + } + }, + { + "label": "portrait-390", + "viewport": { + "width": 390, + "height": 844, + "dpr": 1 + }, + "body": { + "x": 12, + "y": 42.203, + "width": 366, + "height": 759.594, + "right": 378, + "bottom": 801.797, + "centerY": 422 + }, + "header": { + "x": 12, + "y": 42.203, + "width": 366, + "height": 57, + "right": 378, + "bottom": 99.203, + "centerY": 70.703 + }, + "close": { + "x": 330, + "y": 52.203, + "width": 36, + "height": 36, + "right": 366, + "bottom": 88.203, + "centerY": 70.203 + }, + "scroll": { + "top": 0, + "height": 703, + "total": 716, + "rect": { + "x": 12, + "y": 99.203, + "width": 366, + "height": 702.594, + "right": 378, + "bottom": 801.797, + "centerY": 450.5 + } + }, + "image": { + "src": "/fixtures/portrait.svg", + "alt": "portrait.svg", + "naturalWidth": 600, + "naturalHeight": 900, + "box": { + "x": 28, + "y": 215.594, + "width": 319, + "height": 478.5, + "right": 347, + "bottom": 694.094, + "centerY": 454.844 + }, + "paintedWidth": 319, + "paintedHeight": 478.5, + "objectFit": "contain", + "complete": true, + "aspectRatioDelta": 0 + }, + "carouselIndex": "1 / 5", + "focus": { + "tag": "BUTTON", + "ariaLabel": "Закрыть новость", + "className": "feed-news-modal__close" + }, + "actions": { + "likeText": "234", + "likePressed": "true", + "viewsLabel": "Просмотров: 234", + "iconRects": [ + { + "x": 32, + "y": 721.281, + "width": 16, + "height": 16, + "right": 48, + "bottom": 737.281, + "centerY": 729.281 + }, + { + "x": 95.375, + "y": 721.281, + "width": 16, + "height": 16, + "right": 111.375, + "bottom": 737.281, + "centerY": 729.281 + }, + { + "x": 303.625, + "y": 721.281, + "width": 16, + "height": 16, + "right": 319.625, + "bottom": 737.281, + "centerY": 729.281 + } + ], + "centerlineDelta": 0.008 + }, + "files": [], + "fixtureCalls": { + "fetch": [], + "like": [], + "clipboard": [] + } + }, + { + "label": "extreme-portrait-390-top", + "viewport": { + "width": 390, + "height": 844, + "dpr": 1 + }, + "body": { + "x": 12, + "y": 42.203, + "width": 366, + "height": 759.594, + "right": 378, + "bottom": 801.797, + "centerY": 422 + }, + "header": { + "x": 12, + "y": 42.203, + "width": 366, + "height": 57, + "right": 378, + "bottom": 99.203, + "centerY": 70.703 + }, + "close": { + "x": 330, + "y": 52.203, + "width": 36, + "height": 36, + "right": 366, + "bottom": 88.203, + "centerY": 70.203 + }, + "scroll": { + "top": 0, + "height": 703, + "total": 8037, + "rect": { + "x": 12, + "y": 99.203, + "width": 366, + "height": 702.594, + "right": 378, + "bottom": 801.797, + "centerY": 450.5 + } + }, + "image": { + "src": "/fixtures/portrait.svg", + "alt": "portrait.svg", + "naturalWidth": 600, + "naturalHeight": 900, + "box": { + "x": 28, + "y": 7537.328, + "width": 319, + "height": 478.5, + "right": 347, + "bottom": 8015.828, + "centerY": 7776.578 + }, + "paintedWidth": 319, + "paintedHeight": 478.5, + "objectFit": "contain", + "complete": true, + "aspectRatioDelta": 0 + }, + "carouselIndex": "1 / 5", + "focus": { + "tag": "BUTTON", + "ariaLabel": "Закрыть новость", + "className": "feed-news-modal__close" + }, + "actions": { + "likeText": "234", + "likePressed": "true", + "viewsLabel": "Просмотров: 234", + "iconRects": [ + { + "x": 32, + "y": 8043.016, + "width": 16, + "height": 16, + "right": 48, + "bottom": 8059.016, + "centerY": 8051.016 + }, + { + "x": 95.375, + "y": 8043.016, + "width": 16, + "height": 16, + "right": 111.375, + "bottom": 8059.016, + "centerY": 8051.016 + }, + { + "x": 303.625, + "y": 8043.016, + "width": 16, + "height": 16, + "right": 319.625, + "bottom": 8059.016, + "centerY": 8051.016 + } + ], + "centerlineDelta": 0.007 + }, + "files": [], + "fixtureCalls": { + "fetch": [], + "like": [], + "clipboard": [] + } + }, + { + "label": "extreme-portrait-390-bottom", + "viewport": { + "width": 390, + "height": 844, + "dpr": 1 + }, + "body": { + "x": 12, + "y": 42.203, + "width": 366, + "height": 759.594, + "right": 378, + "bottom": 801.797, + "centerY": 422 + }, + "header": { + "x": 12, + "y": 42.203, + "width": 366, + "height": 57, + "right": 378, + "bottom": 99.203, + "centerY": 70.703 + }, + "close": { + "x": 330, + "y": 52.203, + "width": 36, + "height": 36, + "right": 366, + "bottom": 88.203, + "centerY": 70.203 + }, + "scroll": { + "top": 7334, + "height": 703, + "total": 8037, + "rect": { + "x": 12, + "y": 99.203, + "width": 366, + "height": 702.594, + "right": 378, + "bottom": 801.797, + "centerY": 450.5 + } + }, + "image": { + "src": "/fixtures/portrait.svg", + "alt": "portrait.svg", + "naturalWidth": 600, + "naturalHeight": 900, + "box": { + "x": 28, + "y": 203.328, + "width": 319, + "height": 478.5, + "right": 347, + "bottom": 681.828, + "centerY": 442.578 + }, + "paintedWidth": 319, + "paintedHeight": 478.5, + "objectFit": "contain", + "complete": true, + "aspectRatioDelta": 0 + }, + "carouselIndex": "1 / 5", + "focus": { + "tag": "BUTTON", + "ariaLabel": "Закрыть новость", + "className": "feed-news-modal__close" + }, + "actions": { + "likeText": "234", + "likePressed": "true", + "viewsLabel": "Просмотров: 234", + "iconRects": [ + { + "x": 32, + "y": 709.016, + "width": 16, + "height": 16, + "right": 48, + "bottom": 725.016, + "centerY": 717.016 + }, + { + "x": 95.375, + "y": 709.016, + "width": 16, + "height": 16, + "right": 111.375, + "bottom": 725.016, + "centerY": 717.016 + }, + { + "x": 303.625, + "y": 709.016, + "width": 16, + "height": 16, + "right": 319.625, + "bottom": 725.016, + "centerY": 717.016 + } + ], + "centerlineDelta": 0.007 + }, + "files": [], + "fixtureCalls": { + "fetch": [], + "like": [], + "clipboard": [] + } + }, + { + "label": "more-long-no-image-1440", + "viewport": { + "width": 1440, + "height": 900, + "dpr": 1 + }, + "body": { + "x": 280, + "y": 228.547, + "width": 880, + "height": 442.906, + "right": 1160, + "bottom": 671.453, + "centerY": 450 + }, + "header": { + "x": 280, + "y": 228.547, + "width": 880, + "height": 61, + "right": 1160, + "bottom": 289.547, + "centerY": 259.047 + }, + "close": { + "x": 1104, + "y": 240.547, + "width": 36, + "height": 36, + "right": 1140, + "bottom": 276.547, + "centerY": 258.547 + }, + "scroll": { + "top": 0, + "height": 382, + "total": 382, + "rect": { + "x": 280, + "y": 289.547, + "width": 880, + "height": 381.906, + "right": 1160, + "bottom": 671.453, + "centerY": 480.5 + } + }, + "image": null, + "carouselIndex": null, + "focus": { + "tag": "BUTTON", + "ariaLabel": "Закрыть новость", + "className": "feed-news-modal__close" + }, + "actions": { + "likeText": "234", + "likePressed": "true", + "viewsLabel": "Просмотров: 234", + "iconRects": [ + { + "x": 304, + "y": 573.859, + "width": 16, + "height": 16, + "right": 320, + "bottom": 589.859, + "centerY": 581.859 + }, + { + "x": 367.375, + "y": 573.859, + "width": 16, + "height": 16, + "right": 383.375, + "bottom": 589.859, + "centerY": 581.859 + }, + { + "x": 1096.625, + "y": 573.859, + "width": 16, + "height": 16, + "right": 1112.625, + "bottom": 589.859, + "centerY": 581.859 + } + ], + "centerlineDelta": 0.008 + }, + "files": [], + "fixtureCalls": { + "fetch": [ + { + "offset": 6, + "limit": 6, + "type": "vacancy|project|news" + } + ], + "like": [ + { + "ownerType": "profile", + "ownerId": 41, + "newsId": 1, + "state": false + } + ], + "clipboard": ["http://127.0.0.1:4347/office/profile/41/news/1"] + } + } + ], + "interactions": { + "preview": { + "like": { + "modalOpen": false, + "pressed": "false", + "count": "233", + "calls": [ + { + "ownerType": "profile", + "ownerId": 41, + "newsId": 1, + "state": false + } + ] + }, + "share": { + "modalOpen": false, + "clipboardWrites": ["http://127.0.0.1:4347/office/profile/41/news/1"] + }, + "source": { + "modalOpen": false, + "url": "http://127.0.0.1:4347/office/feed" + }, + "cta": { + "modalOpen": false, + "url": "http://127.0.0.1:4347/office/feed" + }, + "structure": { + "shortHasMore": false, + "shortHasMedia": false, + "longHasMore": true, + "ownerMenus": 0 + } + }, + "modalLikeSync": { + "modal": { + "pressed": "false", + "text": "233" + }, + "preview": { + "pressed": "false", + "text": "233" + }, + "calls": [ + { + "ownerType": "profile", + "ownerId": 42, + "newsId": 2, + "state": false + } + ] + }, + "modalShare": { + "modalStillOpen": true, + "clipboardWrites": ["http://127.0.0.1:4347/office/profile/42/news/2"] + }, + "mouseOpen": true, + "keyboard": { + "spaceOpened": true, + "enterOpened": true + }, + "focusReturn": { + "escape": { + "tag": "ARTICLE", + "ariaLabel": "Открыть новость Делюсь иллюстрациями.", + "className": "feed-preview" + }, + "closeButton": "Открыть новость Короткая новость.", + "backdrop": "Открыть новость Короткая новость." + }, + "primaryCarouselModalFetchCalls": [], + "modalUsesExistingFeedData": true + }, + "console": { + "errors": [], + "warnings": [], + "maxErrorsAcrossSnapshots": 0, + "maxWarningsAcrossSnapshots": 0 + }, + "limitations": [ + "Fixture не использует live backend и авторизацию; backend contract проверяется отдельными тестами.", + "Standalone TestBed route подтверждает active/filter fetch, но не считается проверкой production address-bar query synchronization.", + "FeedComponent не передаёт isOwner в NewsCard, поэтому owner dots отсутствуют в браузерном global-feed fixture; их исключение из открытия modal покрывается component regression test.", + "Системное скачивание не запускалось; проверены href и download локального вложения." + ] +} diff --git a/docs/prod-feed-news/screenshots/feed-1024.png b/docs/prod-feed-news/screenshots/feed-1024.png new file mode 100644 index 000000000..397dbd310 Binary files /dev/null and b/docs/prod-feed-news/screenshots/feed-1024.png differ diff --git a/docs/prod-feed-news/screenshots/feed-1280.png b/docs/prod-feed-news/screenshots/feed-1280.png new file mode 100644 index 000000000..2758f386c Binary files /dev/null and b/docs/prod-feed-news/screenshots/feed-1280.png differ diff --git a/docs/prod-feed-news/screenshots/feed-1440.png b/docs/prod-feed-news/screenshots/feed-1440.png new file mode 100644 index 000000000..22b8785d8 Binary files /dev/null and b/docs/prod-feed-news/screenshots/feed-1440.png differ diff --git a/docs/prod-feed-news/screenshots/feed-390.png b/docs/prod-feed-news/screenshots/feed-390.png new file mode 100644 index 000000000..454534b60 Binary files /dev/null and b/docs/prod-feed-news/screenshots/feed-390.png differ diff --git a/docs/prod-feed-news/screenshots/feed-all-1440.png b/docs/prod-feed-news/screenshots/feed-all-1440.png new file mode 100644 index 000000000..d7176fea4 Binary files /dev/null and b/docs/prod-feed-news/screenshots/feed-all-1440.png differ diff --git a/docs/prod-feed-news/screenshots/filter-news-active.png b/docs/prod-feed-news/screenshots/filter-news-active.png new file mode 100644 index 000000000..faeeeeedb Binary files /dev/null and b/docs/prod-feed-news/screenshots/filter-news-active.png differ diff --git a/docs/prod-feed-news/screenshots/filters-1440.png b/docs/prod-feed-news/screenshots/filters-1440.png new file mode 100644 index 000000000..ce088cbb6 Binary files /dev/null and b/docs/prod-feed-news/screenshots/filters-1440.png differ diff --git a/docs/prod-feed-news/screenshots/filters-390-open.png b/docs/prod-feed-news/screenshots/filters-390-open.png new file mode 100644 index 000000000..4aec71b19 Binary files /dev/null and b/docs/prod-feed-news/screenshots/filters-390-open.png differ diff --git a/docs/prod-feed-news/screenshots/image-news-1440.png b/docs/prod-feed-news/screenshots/image-news-1440.png new file mode 100644 index 000000000..c91d5153d Binary files /dev/null and b/docs/prod-feed-news/screenshots/image-news-1440.png differ diff --git a/docs/prod-feed-news/screenshots/long-news-1440.png b/docs/prod-feed-news/screenshots/long-news-1440.png new file mode 100644 index 000000000..cc89c919b Binary files /dev/null and b/docs/prod-feed-news/screenshots/long-news-1440.png differ diff --git a/docs/prod-feed-news/screenshots/long-news-extreme-1440.png b/docs/prod-feed-news/screenshots/long-news-extreme-1440.png new file mode 100644 index 000000000..630dfdb46 Binary files /dev/null and b/docs/prod-feed-news/screenshots/long-news-extreme-1440.png differ diff --git a/docs/prod-feed-news/screenshots/modal-390-long-bottom.png b/docs/prod-feed-news/screenshots/modal-390-long-bottom.png new file mode 100644 index 000000000..a953c82e0 Binary files /dev/null and b/docs/prod-feed-news/screenshots/modal-390-long-bottom.png differ diff --git a/docs/prod-feed-news/screenshots/modal-390-long-top.png b/docs/prod-feed-news/screenshots/modal-390-long-top.png new file mode 100644 index 000000000..10d7af940 Binary files /dev/null and b/docs/prod-feed-news/screenshots/modal-390-long-top.png differ diff --git a/docs/prod-feed-news/screenshots/modal-390.png b/docs/prod-feed-news/screenshots/modal-390.png new file mode 100644 index 000000000..7f9ffe6fc Binary files /dev/null and b/docs/prod-feed-news/screenshots/modal-390.png differ diff --git a/docs/prod-feed-news/screenshots/modal-file-1440.png b/docs/prod-feed-news/screenshots/modal-file-1440.png new file mode 100644 index 000000000..3c6762381 Binary files /dev/null and b/docs/prod-feed-news/screenshots/modal-file-1440.png differ diff --git a/docs/prod-feed-news/screenshots/modal-landscape-1440.png b/docs/prod-feed-news/screenshots/modal-landscape-1440.png new file mode 100644 index 000000000..8f567a8b0 Binary files /dev/null and b/docs/prod-feed-news/screenshots/modal-landscape-1440.png differ diff --git a/docs/prod-feed-news/screenshots/modal-multiple-images-1440.png b/docs/prod-feed-news/screenshots/modal-multiple-images-1440.png new file mode 100644 index 000000000..7d1227ec9 Binary files /dev/null and b/docs/prod-feed-news/screenshots/modal-multiple-images-1440.png differ diff --git a/docs/prod-feed-news/screenshots/modal-portrait-1440.png b/docs/prod-feed-news/screenshots/modal-portrait-1440.png new file mode 100644 index 000000000..807ab2e23 Binary files /dev/null and b/docs/prod-feed-news/screenshots/modal-portrait-1440.png differ diff --git a/docs/prod-feed-news/screenshots/pagination-12.png b/docs/prod-feed-news/screenshots/pagination-12.png new file mode 100644 index 000000000..435ee7537 Binary files /dev/null and b/docs/prod-feed-news/screenshots/pagination-12.png differ diff --git a/docs/prod-feed-news/screenshots/pagination-6.png b/docs/prod-feed-news/screenshots/pagination-6.png new file mode 100644 index 000000000..22b8785d8 Binary files /dev/null and b/docs/prod-feed-news/screenshots/pagination-6.png differ diff --git a/docs/prod-feed-news/screenshots/vacancy-1440.png b/docs/prod-feed-news/screenshots/vacancy-1440.png new file mode 100644 index 000000000..8fe93fed4 Binary files /dev/null and b/docs/prod-feed-news/screenshots/vacancy-1440.png differ diff --git a/projects/core/src/consts/filters/feed-filter.const.ts b/projects/core/src/consts/filters/feed-filter.const.ts index f2d8085c9..6d5a5279e 100644 --- a/projects/core/src/consts/filters/feed-filter.const.ts +++ b/projects/core/src/consts/filters/feed-filter.const.ts @@ -1,6 +1,12 @@ /** @format */ export const feedFilter = [ + { + id: 0, + name: "все новости", + value: "", + icon: "feed", + }, { id: 1, name: "новости проектов", diff --git a/projects/social_platform/src/app/api/feed/facades/feed-info.service.spec.ts b/projects/social_platform/src/app/api/feed/facades/feed-info.service.spec.ts new file mode 100644 index 000000000..7020b7f94 --- /dev/null +++ b/projects/social_platform/src/app/api/feed/facades/feed-info.service.spec.ts @@ -0,0 +1,122 @@ +/** @format */ + +import { ElementRef } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { ActivatedRoute } from "@angular/router"; +import { of } from "rxjs"; +import { FeedInfoService } from "./feed-info.service"; +import { FeedUIInfoService } from "./ui/feed-ui-info.service"; +import { FetchFeedUseCase } from "../use-cases/fetch-feed.use-case"; +import { ReadFeedNewsUseCase } from "../use-cases/read-feed-news.use-case"; +import { ToggleFeedLikeUseCase } from "../use-cases/toggle-feed-like.use-case"; +import { FeedItem } from "@domain/feed/feed-item.model"; +import { ok } from "@domain/shared/result.type"; + +describe("FeedInfoService", () => { + const item = (id: number): FeedItem => ({ + typeModel: "project", + content: { + id, + name: `Проект ${id}`, + shortDescription: "", + industry: 1, + imageAddress: "", + viewsCount: 0, + leader: 1, + }, + }); + + function setup(nextIds: number[]) { + const fetch = { execute: vi.fn() }; + const toggle = { execute: vi.fn().mockReturnValue(of(ok(undefined))) }; + fetch.execute.mockReturnValue( + of( + ok({ + count: 12, + results: nextIds.map(item), + next: "", + previous: "", + }), + ), + ); + TestBed.configureTestingModule({ + providers: [ + FeedInfoService, + FeedUIInfoService, + { provide: ActivatedRoute, useValue: {} }, + { provide: FetchFeedUseCase, useValue: fetch }, + { provide: ReadFeedNewsUseCase, useValue: {} }, + { provide: ToggleFeedLikeUseCase, useValue: toggle }, + ], + }); + const ui = TestBed.inject(FeedUIInfoService); + ui.applyInitializationFeedNewsEvent({ + count: 12, + results: [1, 2, 3, 4, 5, 6].map(item), + next: "", + previous: "", + }); + + const target = document.createElement("div"); + const root = document.createElement("div"); + target.getBoundingClientRect = () => ({ bottom: 600 }) as DOMRect; + root.getBoundingClientRect = () => ({ bottom: 700 }) as DOMRect; + const service = TestBed.inject(FeedInfoService); + service.initScroll(target, new ElementRef(root)); + + return { ui, fetch, toggle, service, target }; + } + + it("догружает шесть элементов в конец в исходном порядке", () => { + const { ui, fetch, target } = setup([7, 8, 9, 10, 11, 12]); + + target.dispatchEvent(new Event("scroll")); + + expect(fetch.execute).toHaveBeenCalledOnce(); + expect(fetch.execute.mock.calls[0].slice(0, 2)).toEqual([6, 6]); + expect(ui.feedItems().map(entry => entry.content.id)).toEqual([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + ]); + expect(ui.feedPage()).toBe(12); + }); + + it("не переставляет старые карточки при дубликате на границе страниц", () => { + const { ui, target } = setup([6, 7, 8, 9, 10, 11]); + + target.dispatchEvent(new Event("scroll")); + + expect(ui.feedItems().map(entry => entry.content.id)).toEqual([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + ]); + expect(ui.feedPage()).toBe(12); + }); + + it("обрабатывает лайк новости при совпадении её id с id проекта", () => { + const { ui, toggle, service } = setup([]); + const news = { + typeModel: "news", + content: { + id: 1, + contentObject: { id: 42, email: "user@example.com" }, + isUserLiked: false, + likesCount: 0, + }, + } as FeedItem; + ui.applyInitializationFeedNewsEvent({ + count: 2, + results: [item(1), news], + next: "", + previous: "", + }); + + service.onLike(1); + + expect(toggle.execute).toHaveBeenCalledOnce(); + expect(ui.feedItems()[0].typeModel).toBe("project"); + const likedItem = ui.feedItems()[1]; + expect(likedItem.typeModel).toBe("news"); + if (likedItem.typeModel === "news") { + expect(likedItem.content.isUserLiked).toBe(true); + } + }); +}); diff --git a/projects/social_platform/src/app/api/feed/facades/feed-info.service.ts b/projects/social_platform/src/app/api/feed/facades/feed-info.service.ts index 8727db7d2..42c409c9a 100644 --- a/projects/social_platform/src/app/api/feed/facades/feed-info.service.ts +++ b/projects/social_platform/src/app/api/feed/facades/feed-info.service.ts @@ -13,8 +13,7 @@ import { tap, throttleTime, } from "rxjs"; -import { FeedItem, FeedItemType } from "@domain/feed/feed-item.model"; -import { ApiPagination } from "@domain/other/api-pagination.model"; +import { FeedItem, FeedItemType, FeedPage } from "@domain/feed/feed-item.model"; import { FeedUIInfoService } from "./ui/feed-ui-info.service"; import { FetchFeedUseCase } from "../use-cases/fetch-feed.use-case"; import { ReadFeedNewsUseCase } from "../use-cases/read-feed-news.use-case"; @@ -49,7 +48,7 @@ export class FeedInfoService { map(r => r["data"]), takeUntilDestroyed(this.destroyRef), ) - .subscribe((feed: ApiPagination) => { + .subscribe((feed: FeedPage) => { this.feedUIInfoService.applyInitializationFeedNewsEvent(feed); this.observer?.disconnect(); @@ -97,18 +96,18 @@ export class FeedInfoService { private onScroll(target: HTMLElement, feedRoot: ElementRef): Observable { if ( - this.feedUIInfoService.totalItemsCount() && - this.feedItems().length >= this.feedUIInfoService.totalItemsCount() + !this.feedUIInfoService.totalItemsCount() || + this.feedUIInfoService.feedPage() >= this.feedUIInfoService.totalItemsCount() ) return EMPTY; if (!target || !feedRoot) return EMPTY; const diff = - target.scrollTop - feedRoot.nativeElement.getBoundingClientRect().height + window.innerHeight; + feedRoot.nativeElement.getBoundingClientRect().bottom - target.getBoundingClientRect().bottom; - if (diff > 0) { - const currentOffset = this.feedItems().length; + if (diff < 400) { + const currentOffset = this.feedUIInfoService.feedPage(); return this.onFetch( currentOffset, @@ -116,12 +115,12 @@ export class FeedInfoService { this.includes(), ).pipe( tap((feedChunk: FeedItem[]) => { + this.feedUIInfoService.feedPage.set(currentOffset + feedChunk.length); const keyOf = (item: FeedItem) => `${item.typeModel}:${item.content.id}`; const existingIds = new Set(this.feedItems().map(keyOf)); const uniqueNewItems = feedChunk.filter(item => !existingIds.has(keyOf(item))); if (uniqueNewItems.length > 0) { - this.feedUIInfoService.feedPage.update(page => page + uniqueNewItems.length); this.feedUIInfoService.feedItems$.update(state => isSuccess(state) ? success([...state.data, ...uniqueNewItems]) @@ -163,6 +162,9 @@ export class FeedInfoService { return this.fetchFeedUseCase.execute(offset, limit, type).pipe( tap(result => { this.feedUIInfoService.totalItemsCount.set(result.ok ? result.value.count : 0); + if (result.ok && result.value.counts) { + this.feedUIInfoService.categoryCounts.set(result.value.counts); + } }), map(result => (result.ok ? result.value.results : [])), ); @@ -170,10 +172,9 @@ export class FeedInfoService { private onFeedItemView(entries: IntersectionObserverEntry[]): void { const items = entries - .map(e => { - return Number((e.target as HTMLElement).dataset["id"]); - }) - .map(id => this.feedItems().find(item => item.content.id === id)) + .filter(entry => entry.isIntersecting) + .map(e => (e.target as HTMLElement).dataset["key"]) + .map(key => this.feedItems().find(item => `${item.typeModel}:${item.content.id}` === key)) .filter(Boolean) as FeedItem[]; const projectNews = items.filter( @@ -203,7 +204,9 @@ export class FeedInfoService { } onLike(newsId: number) { - const itemIdx = this.feedItems().findIndex(n => n.content.id === newsId); + const itemIdx = this.feedItems().findIndex( + n => n.typeModel === "news" && n.content.id === newsId, + ); const item = this.feedItems()[itemIdx]; if (!item || item.typeModel !== "news") return; diff --git a/projects/social_platform/src/app/api/feed/facades/ui/feed-ui-info.service.spec.ts b/projects/social_platform/src/app/api/feed/facades/ui/feed-ui-info.service.spec.ts new file mode 100644 index 000000000..01c986c60 --- /dev/null +++ b/projects/social_platform/src/app/api/feed/facades/ui/feed-ui-info.service.spec.ts @@ -0,0 +1,25 @@ +/** @format */ +import { FeedNews } from "@domain/news/project-news.model"; +import { FeedItem } from "@domain/feed/feed-item.model"; +import { FeedUIInfoService } from "./feed-ui-info.service"; + +describe("FeedUIInfoService: единый input карточки и окна", () => { + it("лайк обновляет только нужную новость, сохраняя порядок, page size и counts", () => { + const ui = new FeedUIInfoService(); + const results: FeedItem[] = [1, 2, 3, 4, 5, 6].map(id => ({ + typeModel: "news", + content: { ...FeedNews.default(), id, isUserLiked: false, likesCount: 0 }, + })); + const counts = { all: 86, project: 56, vacancy: 3, news: 27, partnerprogram: 0, education: 0 }; + ui.applyInitializationFeedNewsEvent({ count: 86, results, counts }); + ui.applyLikeNews(1); + expect(ui.feedItems().map(item => item.content.id)).toEqual([1, 2, 3, 4, 5, 6]); + expect(ui.feedItems()[0]).toBe(results[0]); + expect(ui.feedItems()[1].content).toMatchObject({ isUserLiked: true, likesCount: 1 }); + expect(ui.categoryCounts()).toBe(counts); + expect(ui.perFetchTake()).toBe(6); + expect(ui.feedPage()).toBe(6); + ui.applyLikeNews(1); + expect(ui.feedItems()[1].content).toMatchObject({ isUserLiked: false, likesCount: 0 }); + }); +}); diff --git a/projects/social_platform/src/app/api/feed/facades/ui/feed-ui-info.service.ts b/projects/social_platform/src/app/api/feed/facades/ui/feed-ui-info.service.ts index e67ea92d7..be49b9188 100644 --- a/projects/social_platform/src/app/api/feed/facades/ui/feed-ui-info.service.ts +++ b/projects/social_platform/src/app/api/feed/facades/ui/feed-ui-info.service.ts @@ -1,8 +1,7 @@ /** @format */ import { computed, Injectable, signal } from "@angular/core"; -import { ApiPagination } from "@domain/other/api-pagination.model"; -import { FeedItem } from "@domain/feed/feed-item.model"; +import { FeedCategoryCounts, FeedItem, FeedPage } from "@domain/feed/feed-item.model"; import { AsyncState, initial, isLoading, isSuccess, success } from "@domain/shared/async-state"; /** UI-проекция ленты: computed-сигналы страницы `FeedItem`. */ @@ -18,13 +17,22 @@ export class FeedUIInfoService { }); readonly totalItemsCount = signal(0); + readonly categoryCounts = signal({ + all: 0, + project: 0, + vacancy: 0, + news: 0, + partnerprogram: 0, + education: 0, + }); readonly feedPage = signal(0); - readonly perFetchTake = signal(20); + readonly perFetchTake = signal(6); - applyInitializationFeedNewsEvent(feed: ApiPagination): void { + applyInitializationFeedNewsEvent(feed: FeedPage): void { this.feedItems$.set(success(feed.results)); this.totalItemsCount.set(feed.count); this.feedPage.set(feed.results.length); + if (feed.counts) this.categoryCounts.set(feed.counts); } applyFeedFilters(feed: FeedItem[]): void { diff --git a/projects/social_platform/src/app/api/feed/use-cases/fetch-feed.use-case.ts b/projects/social_platform/src/app/api/feed/use-cases/fetch-feed.use-case.ts index 235d1636b..857c85b98 100644 --- a/projects/social_platform/src/app/api/feed/use-cases/fetch-feed.use-case.ts +++ b/projects/social_platform/src/app/api/feed/use-cases/fetch-feed.use-case.ts @@ -3,8 +3,7 @@ import { inject, Injectable } from "@angular/core"; import { catchError, map, Observable, of } from "rxjs"; import { FeedRepositoryPort } from "@domain/feed/ports/feed.repository.port"; -import { FeedItem } from "@domain/feed/feed-item.model"; -import { ApiPagination } from "@domain/other/api-pagination.model"; +import { FeedPage } from "@domain/feed/feed-item.model"; import { fail, ok, Result } from "@domain/shared/result.type"; /** Сценарий: страница ленты (offset/limit/type); ошибка → `fetch_feed_error`. */ @@ -16,9 +15,9 @@ export class FetchFeedUseCase { offset: number, limit: number, type: string, - ): Observable, { kind: "fetch_feed_error"; cause?: unknown }>> { + ): Observable> { return this.feedRepositoryPort.fetchFeed(offset, limit, type).pipe( - map(feed => ok>(feed)), + map(feed => ok(feed)), catchError(error => of(fail({ kind: "fetch_feed_error" as const, cause: error }))), ); } diff --git a/projects/social_platform/src/app/domain/feed/feed-item.model.ts b/projects/social_platform/src/app/domain/feed/feed-item.model.ts index f2f4d1ce8..1a16e689a 100644 --- a/projects/social_platform/src/app/domain/feed/feed-item.model.ts +++ b/projects/social_platform/src/app/domain/feed/feed-item.model.ts @@ -2,6 +2,7 @@ import { FeedNews } from "../news/project-news.model"; import { Vacancy } from "../vacancy/vacancy.model"; +import { ApiPagination } from "../other/api-pagination.model"; /** Модели данных для элементов ленты */ /** Интерфейс проекта в ленте */ @@ -20,9 +21,28 @@ export type FeedItemType = "vacancy" | "news" | "project"; /** Объединенный тип элемента ленты */ export type FeedItem = - | ({ typeModel: FeedItemType } & { + | ({ typeModel: FeedItemType; publishedAt?: string } & { typeModel: "project"; content: FeedProject; }) - | { typeModel: "vacancy"; content: Vacancy } - | { typeModel: "news"; content: FeedNews & { contentObject: { id: number } } }; + | { typeModel: "vacancy"; content: Vacancy; publishedAt?: string } + | { + typeModel: "news"; + content: FeedNews & { contentObject: { id: number; email?: string; industry?: number } }; + publishedAt?: string; + }; + +/** Счётчики всех доступных категорий, независимо от выбранной страницы. */ +export interface FeedCategoryCounts { + all: number; + project: number; + vacancy: number; + news: number; + partnerprogram: number; + education: number; +} + +/** Страница ленты с метаданными категорий. */ +export interface FeedPage extends ApiPagination { + counts?: FeedCategoryCounts; +} diff --git a/projects/social_platform/src/app/domain/feed/ports/feed.repository.port.ts b/projects/social_platform/src/app/domain/feed/ports/feed.repository.port.ts index 923ef544a..55673038f 100644 --- a/projects/social_platform/src/app/domain/feed/ports/feed.repository.port.ts +++ b/projects/social_platform/src/app/domain/feed/ports/feed.repository.port.ts @@ -1,14 +1,9 @@ /** @format */ import { Observable } from "rxjs"; -import { ApiPagination } from "../../other/api-pagination.model"; -import { FeedItem } from "../feed-item.model"; +import { FeedPage } from "../feed-item.model"; /** Порт репозитория ленты: страница `FeedItem` (offset/limit/type). */ export abstract class FeedRepositoryPort { - abstract fetchFeed( - offset: number, - limit: number, - type: string, - ): Observable>; + abstract fetchFeed(offset: number, limit: number, type: string): Observable; } diff --git a/projects/social_platform/src/app/infrastructure/adapters/feed/feed-http.adapter.ts b/projects/social_platform/src/app/infrastructure/adapters/feed/feed-http.adapter.ts index 72d8694aa..2b67ebcd2 100644 --- a/projects/social_platform/src/app/infrastructure/adapters/feed/feed-http.adapter.ts +++ b/projects/social_platform/src/app/infrastructure/adapters/feed/feed-http.adapter.ts @@ -4,8 +4,7 @@ import { inject, Injectable } from "@angular/core"; import { ApiService } from "@corelib"; import { Observable } from "rxjs"; import { HttpParams } from "@angular/common/http"; -import { ApiPagination } from "@domain/other/api-pagination.model"; -import { FeedItem } from "@domain/feed/feed-item.model"; +import { FeedPage } from "@domain/feed/feed-item.model"; /** HTTP-адаптер ленты: `/feed` (offset/limit/type). */ @Injectable({ providedIn: "root" }) @@ -14,8 +13,8 @@ export class FeedHttpAdapter { private readonly apiService = inject(ApiService); - fetchFeed(offset: number, limit: number, type: string): Observable> { - return this.apiService.get>( + fetchFeed(offset: number, limit: number, type: string): Observable { + return this.apiService.get( `${this.FEED_URL}/`, new HttpParams({ fromObject: { limit, offset, type } }), ); diff --git a/projects/social_platform/src/app/infrastructure/repository/feed/feed.repository.ts b/projects/social_platform/src/app/infrastructure/repository/feed/feed.repository.ts index cd5468ae8..552ca354c 100644 --- a/projects/social_platform/src/app/infrastructure/repository/feed/feed.repository.ts +++ b/projects/social_platform/src/app/infrastructure/repository/feed/feed.repository.ts @@ -2,8 +2,7 @@ import { inject, Injectable } from "@angular/core"; import { Observable } from "rxjs"; -import { ApiPagination } from "@domain/other/api-pagination.model"; -import { FeedItem } from "@domain/feed/feed-item.model"; +import { FeedPage } from "@domain/feed/feed-item.model"; import { FeedRepositoryPort } from "@domain/feed/ports/feed.repository.port"; import { FeedHttpAdapter } from "../../adapters/feed/feed-http.adapter"; @@ -12,7 +11,7 @@ import { FeedHttpAdapter } from "../../adapters/feed/feed-http.adapter"; export class FeedRepository implements FeedRepositoryPort { private readonly feedAdapter = inject(FeedHttpAdapter); - fetchFeed(offset: number, limit: number, type: string): Observable> { + fetchFeed(offset: number, limit: number, type: string): Observable { return this.feedAdapter.fetchFeed(offset, limit, type); } } diff --git a/projects/social_platform/src/app/ui/pages/feed/feed.component.html b/projects/social_platform/src/app/ui/pages/feed/feed.component.html index bf435a607..d8558d3a6 100644 --- a/projects/social_platform/src/app/ui/pages/feed/feed.component.html +++ b/projects/social_platform/src/app/ui/pages/feed/feed.component.html @@ -2,28 +2,33 @@
-
+
@if (feedItems().length > 0) { - @for (item of feedItems(); track item.content.id) { + @for (item of feedItems(); track item.typeModel + ":" + item.content.id) { @if (item.typeModel === "vacancy") { } @else if (item.typeModel === "project") { } @else if (item.typeModel === "news") { } diff --git a/projects/social_platform/src/app/ui/pages/feed/feed.component.scss b/projects/social_platform/src/app/ui/pages/feed/feed.component.scss index 7ae1bc2fb..bb606b5b7 100644 --- a/projects/social_platform/src/app/ui/pages/feed/feed.component.scss +++ b/projects/social_platform/src/app/ui/pages/feed/feed.component.scss @@ -1,3 +1,5 @@ +/** @format */ + @use "styles/responsive"; .page { @@ -7,12 +9,19 @@ padding-top: 40px; &__feed { - column-count: 2; - column-gap: 20px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px; + align-items: stretch; + + @media (max-width: 999px) { + grid-template-columns: minmax(0, 1fr); + } &--no-items { display: flex; flex-direction: column; + grid-column: 1 / -1; gap: 20px; align-items: center; justify-content: center; @@ -26,9 +35,7 @@ } &__item { - display: block; - width: 100%; - margin-bottom: 20px; - break-inside: avoid; + display: grid; + min-width: 0; } } diff --git a/projects/social_platform/src/app/ui/pages/feed/feed.component.spec.ts b/projects/social_platform/src/app/ui/pages/feed/feed.component.spec.ts new file mode 100644 index 000000000..db5e28de0 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/feed/feed.component.spec.ts @@ -0,0 +1,153 @@ +/** @format */ + +import { By } from "@angular/platform-browser"; +import { firstValueFrom } from "rxjs"; +import { ModalComponent } from "@ui/primitives/modal/modal.component"; +import { TestBed } from "@angular/core/testing"; +import { provideRouter } from "@angular/router"; +import { HttpClientTestingModule } from "@angular/common/http/testing"; +import { API_URL } from "@corelib"; +import { FeedInfoService } from "@api/feed/facades/feed-info.service"; +import { FeedUIInfoService } from "@api/feed/facades/ui/feed-ui-info.service"; +import { FeedItem, FeedProject } from "@domain/feed/feed-item.model"; +import { FeedNews } from "@domain/news/project-news.model"; +import { Vacancy } from "@domain/vacancy/vacancy.model"; +import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; +import { success } from "@domain/shared/async-state"; +import { FeedComponent } from "./feed.component"; + +describe("FeedComponent: порядок и представление карточек #372", () => { + const project: FeedProject = { + id: 1, + name: "Проект", + shortDescription: "Описание", + imageAddress: "", + industry: 1, + leader: 7, + viewsCount: 0, + }; + const date = "2026-08-29T12:00:00Z"; + const news = { + ...FeedNews.default(), + id: 1, + name: "Источник", + text: "Первая строка. Подробности", + files: [], + contentObject: { id: 1, industry: 1 }, + }; + const initial: FeedItem[] = [ + { + typeModel: "vacancy", + publishedAt: date, + content: { + id: 1, + project, + role: "QA", + description: "Задачи", + requiredSkills: [], + } as unknown as Vacancy, + }, + { typeModel: "news", publishedAt: date, content: news }, + { typeModel: "project", publishedAt: date, content: project }, + { + typeModel: "news", + publishedAt: date, + content: { ...news, id: 2, contentObject: { id: 42, email: "fixture@example.test" } }, + }, + { typeModel: "project", publishedAt: date, content: { ...project, id: 2 } }, + { typeModel: "project", publishedAt: date, content: { ...project, id: 3 } }, + ]; + + it("сохраняет шесть начальных карточек, ключи type:id и append в DOM без перестановки", async () => { + const ui = new FeedUIInfoService(); + ui.applyInitializationFeedNewsEvent({ count: 12, next: "", previous: "", results: initial }); + await TestBed.configureTestingModule({ + imports: [FeedComponent, HttpClientTestingModule], + providers: [ + provideRouter([]), + { provide: API_URL, useValue: "" }, + { provide: IndustryRepositoryPort, useValue: { getOne: () => ({ name: "IT" }) } }, + ], + }) + .overrideComponent(FeedComponent, { + set: { + providers: [ + { provide: FeedUIInfoService, useValue: ui }, + { + provide: FeedInfoService, + useValue: { + initializationFeedNews: vi.fn(), + initScroll: vi.fn(), + destroy: vi.fn(), + onLike(id: number) { + ui.applyLikeNews( + ui + .feedItems() + .findIndex(item => item.typeModel === "news" && item.content.id === id), + ); + }, + }, + }, + ], + }, + }) + .compileComponents(); + const fixture = TestBed.createComponent(FeedComponent); + fixture.detectChanges(); + const root: HTMLElement = fixture.nativeElement; + const cards = () => Array.from(root.querySelectorAll(".page__item")); + const before = cards(); + expect(before.map(card => card.dataset["key"])).toEqual([ + "vacancy:1", + "news:1", + "project:1", + "news:2", + "project:2", + "project:3", + ]); + expect( + before.slice(0, 4).map(card => card.querySelector(".feed-card__type")?.textContent?.trim()), + ).toEqual(["Новая вакансия", "Новости проекта", "Новый проект", "Новости людей"]); + expect( + before.every(card => card.querySelector("time")?.getAttribute("datetime") === date), + ).toBe(true); + expect(before[3].querySelector(".feed-card__cta")?.getAttribute("href")).toBe( + "/office/profile/42", + ); + + const next: FeedItem[] = [4, 5, 6, 7, 8, 9].map(id => ({ + typeModel: "project", + content: { ...project, id }, + publishedAt: date, + })); + ui.feedItems$.set(success([...initial, ...next])); + fixture.detectChanges(); + expect(cards()).toHaveLength(12); + expect(cards().slice(0, 6)).toEqual(before); + expect( + cards() + .slice(6) + .map(card => card.dataset["key"]), + ).toEqual(["project:4", "project:5", "project:6", "project:7", "project:8", "project:9"]); + expect(ui.perFetchTake()).toBe(6); + const article = root.querySelector('[data-key="news:1"] article')!; + article.click(); + fixture.detectChanges(); + const primitive = fixture.debugElement.query(By.directive(ModalComponent)) + .componentInstance as ModalComponent; + await firstValueFrom(primitive.overlayRef!.attachments()); + await fixture.whenStable(); + const dialog = document.querySelector('[role="dialog"]')!; + const previous = initial[1].content as FeedNews; + dialog.querySelector('[aria-label="Нравится"]')!.click(); + await fixture.whenStable(); + expect(article.querySelector('[aria-label="Нравится"]')?.getAttribute("aria-pressed")).toBe( + String(!previous.isUserLiked), + ); + expect(dialog.querySelector('[aria-label="Нравится"]')?.getAttribute("aria-pressed")).toBe( + String(!previous.isUserLiked), + ); + expect((ui.feedItems()[1].content as FeedNews).viewsCount).toBe(previous.viewsCount); + expect(cards()).toHaveLength(12); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/feed/feed.component.ts b/projects/social_platform/src/app/ui/pages/feed/feed.component.ts index 764ac0709..f1ba9d926 100644 --- a/projects/social_platform/src/app/ui/pages/feed/feed.component.ts +++ b/projects/social_platform/src/app/ui/pages/feed/feed.component.ts @@ -4,13 +4,10 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, - computed, ElementRef, inject, OnDestroy, - OnInit, viewChild, - ViewChild, } from "@angular/core"; import { CommonModule } from "@angular/common"; import { NewsCardComponent } from "@ui/widgets/news-card/news-card.component"; @@ -22,6 +19,7 @@ import { FeedInfoService } from "@api/feed/facades/feed-info.service"; import { FeedUIInfoService } from "@api/feed/facades/ui/feed-ui-info.service"; import { AppRoutes } from "@api/paths/app-routes"; import { ProjectTeamUIService } from "@api/project/facades/edit/ui/project-team-ui.service"; +import { FeedItem } from "@domain/feed/feed-item.model"; /** Страница ленты активности. */ @Component({ @@ -36,11 +34,10 @@ import { ProjectTeamUIService } from "@api/project/facades/edit/ui/project-team- FeedFilterComponent, NewsCardComponent, OpenVacancyComponent, - IconComponent, ], providers: [FeedInfoService, FeedUIInfoService, ProjectTeamUIService], }) -export class FeedComponent implements OnInit, AfterViewInit, OnDestroy { +export class FeedComponent implements AfterViewInit, OnDestroy { readonly feedRoot = viewChild>("feedRoot"); private readonly feedInfoService = inject(FeedInfoService); @@ -50,22 +47,28 @@ export class FeedComponent implements OnInit, AfterViewInit, OnDestroy { protected readonly AppRoutes = AppRoutes; /** Ссылка-маршрут для карточки новости в зависимости от типа источника. */ - protected resourceLink(content: any): (string | number)[] { - if (content.contentObject && "email" in content.contentObject) { - return [AppRoutes.profile.detail(content.contentObject.id)]; + protected resourceLink(item: FeedItem): (string | number)[] { + if (item.typeModel !== "news") return []; + const source = item.content.contentObject; + if ("email" in source) { + return [AppRoutes.profile.detail(source.id)]; } - return [AppRoutes.projects.detail(content.contentObject.id)]; + return [AppRoutes.projects.detail(source.id)]; } - ngOnInit() { - this.feedInfoService.initializationFeedNews(this.feedRoot()!); + protected isPersonNews(item: FeedItem): boolean { + return item.typeModel === "news" && "email" in item.content.contentObject; } ngAfterViewInit() { const target = document.querySelector(".office__body") as HTMLElement; - if (target || this.feedRoot) { - this.feedInfoService.initScroll(target, this.feedRoot()!); + const root = this.feedRoot(); + if (root) { + this.feedInfoService.initializationFeedNews(root); + } + if (target && root) { + this.feedInfoService.initScroll(target, root); } } diff --git a/projects/social_platform/src/app/ui/pages/feed/feed.resolver.ts b/projects/social_platform/src/app/ui/pages/feed/feed.resolver.ts index 108db63a3..bd624010a 100644 --- a/projects/social_platform/src/app/ui/pages/feed/feed.resolver.ts +++ b/projects/social_platform/src/app/ui/pages/feed/feed.resolver.ts @@ -3,21 +3,20 @@ import { inject } from "@angular/core"; import { ResolveFn } from "@angular/router"; import { map } from "rxjs"; -import { ApiPagination } from "@domain/other/api-pagination.model"; -import { FeedItem, FeedItemType } from "@domain/feed/feed-item.model"; +import { FeedItemType, FeedPage } from "@domain/feed/feed-item.model"; import { FetchFeedUseCase } from "@api/feed/use-cases/fetch-feed.use-case"; import { FILTER_SPLIT_SYMBOL } from "@core/consts/other/filter-split-symbol.const"; const DEFAULT_FEED_TYPES: FeedItemType[] = ["vacancy", "news", "project"]; /** Предзагружает ленту новостей. */ -export const FeedResolver: ResolveFn> = route => { +export const FeedResolver: ResolveFn = route => { const fetchFeedUseCase = inject(FetchFeedUseCase); - // Загружаем первую страницу ленты (offset: 0, limit: 20) + // Загружаем первую страницу ленты тремя рядами по две карточки. // По умолчанию включаем вакансии, новости и проекты return fetchFeedUseCase - .execute(0, 20, route.queryParams["includes"] ?? DEFAULT_FEED_TYPES.join(FILTER_SPLIT_SYMBOL)) + .execute(0, 6, route.queryParams["includes"] ?? DEFAULT_FEED_TYPES.join(FILTER_SPLIT_SYMBOL)) .pipe( map(result => result.ok diff --git a/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.html b/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.html index efbf27e9e..fc50c49e4 100644 --- a/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.html +++ b/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.html @@ -1,63 +1,37 @@ - -
-
- - -
-
-

- {{ feedItem().name | truncate: 30 }} -

- -
- -
-

- @if (industryRepository.getOne(feedItem().industry); as industry) { - - {{ industry.name }} - - } -

- - -
- -

{{ feedItem().shortDescription }}

+
+
+ Новый проект + @if (publishedAt()) { + + } +
+
+ +
+ Проект + {{ feedItem().name }}
- -
- поддержать проект - - перейти в проект +

{{ feedItem().name }}

+

{{ feedItem().shortDescription }}

+ -
+ Перейти в проект +
diff --git a/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.scss b/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.scss index 72abb7cf1..a797d59d8 100644 --- a/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.scss +++ b/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.scss @@ -1,64 +1,13 @@ -@use "styles/typography"; -@use "styles/responsive"; +/** @format */ -.card { - padding: 37px 24px 24px; - color: var(--light-white); - background-color: var(--accent); - border-radius: var(--rounded-xl); - - &__avatar { - display: flex; - align-items: center; - justify-content: center; - text-align: center; - } - - &__project { - display: grid; - grid-template-columns: 2fr 3fr; - grid-gap: 40px; - margin-bottom: 20px; - } - - &__info { - display: flex; - flex-direction: column; - - &:first-child { - margin-bottom: 4px; - } +@use "styles/feed-card"; - &--main, - &--additional { - display: flex; - align-items: center; - justify-content: space-between; - } - } - - &__industry { - display: inline-flex; - } - - &__actions { - display: flex; - gap: 10px; - align-items: center; - width: 100%; - } - - &__score { - display: flex; - gap: 3px; - align-items: center; - } +.card { + --feed-background: var(--accent); + --feed-text: var(--white); + --feed-secondary: var(--white); + --feed-cta-text: var(--accent-dark); + --feed-cta-background: var(--white); - &__description { - margin-top: 7px; - color: var(--white); - word-break: break-word; - overflow-wrap: anywhere; - white-space: wrap; - } + @include feed-card.layout; } diff --git a/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.spec.ts b/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.spec.ts new file mode 100644 index 000000000..16e9c260d --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.spec.ts @@ -0,0 +1,42 @@ +/** @format */ + +import { TestBed } from "@angular/core/testing"; +import { RouterTestingModule } from "@angular/router/testing"; +import { FeedProject } from "@domain/feed/feed-item.model"; +import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; +import { NewProjectComponent } from "./new-project.component"; + +describe("NewProjectComponent", () => { + it("показывает тип, дату, название, описание, индустрию и CTA", () => { + TestBed.configureTestingModule({ + imports: [NewProjectComponent, RouterTestingModule], + providers: [ + { provide: IndustryRepositoryPort, useValue: { getOne: () => ({ name: "Инженерия" }) } }, + ], + }); + const fixture = TestBed.createComponent(NewProjectComponent); + fixture.componentRef.setInput("feedItem", { + id: 5, + name: "Проект Б", + shortDescription: "Краткое описание проекта", + industry: 1, + imageAddress: "", + viewsCount: 0, + leader: 2, + } satisfies FeedProject); + fixture.componentRef.setInput("publishedAt", "2026-09-23T12:00:00Z"); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + expect(element.querySelector(".card__type")?.textContent?.trim()).toBe("Новый проект"); + expect(element.querySelector(".card__date")?.textContent?.trim()).toBe("23.09.2026"); + expect(element.querySelector(".card__title")?.textContent?.trim()).toBe("Проект Б"); + expect(element.querySelector(".card__description")?.textContent?.trim()).toBe( + "Краткое описание проекта", + ); + expect(element.querySelector(".card__industry")?.textContent?.trim()).toBe("Инженерия"); + expect(element.querySelector(".card__cta")?.textContent).toContain("Перейти в проект"); + expect(element.querySelector("a.card__cta")?.getAttribute("href")).toBe("/office/projects/5"); + expect(element.querySelector("time")?.getAttribute("datetime")).toBe("2026-09-23T12:00:00Z"); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.ts b/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.ts index ddb19c4fd..46646eef0 100644 --- a/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.ts +++ b/projects/social_platform/src/app/ui/pages/feed/new-project/new-project.component.ts @@ -1,28 +1,25 @@ /** @format */ -import { ChangeDetectionStrategy, Component, input, Input } from "@angular/core"; -import { CommonModule } from "@angular/common"; -import { ButtonComponent, IconComponent } from "@ui/primitives"; -import { AvatarComponent } from "@ui/primitives/avatar/avatar.component"; -import { Router, RouterLink } from "@angular/router"; -import { TagComponent } from "@ui/primitives/tag/tag.component"; -import { TruncatePipe, DayjsPipe } from "@corelib"; +import { ChangeDetectionStrategy, Component, inject, input } from "@angular/core"; +import { RouterLink } from "@angular/router"; +import { DayjsPipe } from "@corelib"; import { FeedProject } from "@domain/feed/feed-item.model"; import { AppRoutes } from "@api/paths/app-routes"; import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; +import { AvatarComponent } from "@ui/primitives/avatar/avatar.component"; -/** Карточка нового проекта в ленте новостей. */ +/** Карточка нового проекта в общей ленте. */ @Component({ selector: "app-new-project", - imports: [CommonModule, ButtonComponent, AvatarComponent, RouterLink, TruncatePipe, TagComponent], + imports: [RouterLink, DayjsPipe, AvatarComponent], templateUrl: "./new-project.component.html", styleUrl: "./new-project.component.scss", changeDetection: ChangeDetectionStrategy.OnPush, }) export class NewProjectComponent { readonly feedItem = input.required(); + readonly publishedAt = input(""); protected readonly AppRoutes = AppRoutes; - - constructor(public readonly industryRepository: IndustryRepositoryPort) {} + protected readonly industryRepository = inject(IndustryRepositoryPort); } diff --git a/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.html b/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.html index 0b903ae13..c4ee6e578 100644 --- a/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.html +++ b/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.html @@ -1,113 +1,61 @@ - -@if (feedItem()) { -
-
- vacancy-card-background - @if (feedItem().project; as project) { -
- - -

- {{ project.name | truncate: 30 }} -

- - @if (industryRepositoryGetOne(project.industry); as industry) { - - {{ industry.name }} - - } -
- } - @if (feedItem(); as vacancy) { -
-
-

- {{ vacancy.role | truncate: 30 }} -

-

{{ vacancy.datetimeCreated | dayjs: "format" : "DD MM YY" }}

-
- -
- @if (vacancy.requiredSkills.length; as skillsLength) { - @if (vacancy.requiredSkills; as requiredSkills) { - @if (requiredSkills) { -
    - @for (skill of requiredSkills.slice(0, 3); track $index) { - {{ skill.name }} - } -
- } -
- @if (requiredSkills) { -
    - @for (skill of requiredSkills.slice(8); track $index) { - {{ skill.name }} - } -
- } -
- } - @if (skillsLength > 8) { -
- {{ readFullSkills ? "cкрыть" : "подробнее" }} -
- } - } -
- - @if (feedItem().description) { -
-
-

- @if (descriptionExpandable()) { -
- {{ readFullDescription() ? "скрыть" : "подробнее" }} -
- } -
-
- } -
- } -
- - @if (feedItem()) { -
- перейти в проект - откликнуться на вакансию +
+ Новая вакансия + @if (publishedAt()) { + + } +
+
+ @if (feedItem().project; as project) { + +
+ Проект / компания + {{ project.name }}
}
-} +

{{ feedItem().role }}

+

+ + Подробнее о вакансии + diff --git a/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.scss b/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.scss index b83b39471..0d0b83c98 100644 --- a/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.scss +++ b/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.scss @@ -1,191 +1,7 @@ -@use "styles/responsive"; -@use "styles/typography"; +/** @format */ -@mixin expandable-list { - &__remaining { - display: grid; - grid-template-rows: 0fr; - overflow: hidden; - transition: all 0.5s ease-in-out; - - &--show { - grid-template-rows: 1fr; - margin-top: 12px; - } - - ul { - min-height: 0; - } - - li { - &:first-child { - margin-top: 12px; - } - - &:not(:last-child) { - margin-bottom: 12px; - } - } - } -} +@use "styles/feed-card"; .card { - position: relative; - z-index: 100; - padding: 27px 24px 24px; - overflow-y: hidden; - background-color: var(--light-white); - border: 0.5px solid var(--medium-grey-for-outline); - border-radius: var(--rounded-xl); - - &__inner { - display: grid; - grid-template-columns: 2fr 3fr; - gap: 10px; - align-items: flex-start; - } - - &__background { - position: absolute; - top: 0; - left: 0; - z-index: 0; - } - - &__industry { - display: inline-flex; - } - - &__description { - word-break: break-word; - overflow-wrap: anywhere; - white-space: wrap; - } - - &__project { - display: flex; - flex-direction: column; - gap: 6px; - align-items: center; - text-align: center; - } - - &__vacancy { - display: flex; - flex-direction: column; - gap: 8px; - } - - &__actions { - display: flex; - gap: 10px; - align-items: center; - margin-top: 15px; - - &:last-child { - ::ng-deep { - app-button { - .button--big { - padding: 4px 16px; - } - } - } - } - } -} - -.lists { - &__section { - display: flex; - justify-content: space-between; - margin-bottom: 8px; - border-bottom: 0.5px solid var(--accent); - } - - &__icon { - color: var(--accent); - } - - &__title { - margin-bottom: 8px; - color: var(--accent); - } - - &__item { - display: flex; - gap: 6px; - align-items: center; - } -} - -.about { - - /* stylelint-disable value-no-vendor-prefix */ - &__text { - p { - display: -webkit-box; - overflow: hidden; - color: var(--black); - text-overflow: ellipsis; - -webkit-box-orient: vertical; - -webkit-line-clamp: 5; - transition: all 0.7s ease-in-out; - - &.expanded { - -webkit-line-clamp: unset; - } - } - - ::ng-deep a { - color: var(--accent); - text-decoration: underline; - text-decoration-color: transparent; - text-underline-offset: 3px; - transition: text-decoration-color 0.2s; - - &:hover { - text-decoration-color: var(--accent); - } - } - } - - /* stylelint-enable value-no-vendor-prefix */ - - &__read-full { - margin-top: 2px; - color: var(--accent); - cursor: pointer; - } -} - -.read-more { - margin-top: 12px; - color: var(--accent); - cursor: pointer; - transition: color 0.2s; - - &:hover { - color: var(--accent-dark); - } -} - -.skills { - &__title { - margin-bottom: 5px; - color: var(--dark-grey); - } - - &__list { - display: flex; - flex-wrap: wrap; - gap: 10px; - } - - li { - &:not(:last-child) { - margin-bottom: 12px; - } - } - - @include expandable-list; + @include feed-card.layout; } diff --git a/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.spec.ts b/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.spec.ts new file mode 100644 index 000000000..8c147c26e --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.spec.ts @@ -0,0 +1,65 @@ +/** @format */ + +import { TestBed } from "@angular/core/testing"; +import { RouterTestingModule } from "@angular/router/testing"; +import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; +import { Vacancy } from "@domain/vacancy/vacancy.model"; +import { OpenVacancyComponent } from "./open-vacancy.component"; + +describe("OpenVacancyComponent", () => { + it("показывает тип, дату, источник, роль, описание, навыки и CTA", () => { + TestBed.configureTestingModule({ + imports: [OpenVacancyComponent, RouterTestingModule], + providers: [ + { provide: IndustryRepositoryPort, useValue: { getOne: () => ({ name: "IT" }) } }, + ], + }); + const fixture = TestBed.createComponent(OpenVacancyComponent); + fixture.componentRef.setInput("feedItem", { + id: 7, + role: "Frontend-разработчик", + description: "Создание интерфейсов для платформы.", + project: { id: 3, name: "Проект А", imageAddress: "", industry: 1 }, + requiredSkills: [ + { id: 1, name: "Angular" }, + { id: 2, name: "TypeScript" }, + ], + } as Vacancy); + fixture.componentRef.setInput("publishedAt", "2026-09-23T12:00:00Z"); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + expect(element.querySelector(".card__type")?.textContent?.trim()).toBe("Новая вакансия"); + expect(element.querySelector(".card__date")?.textContent?.trim()).toBe("23.09.2026"); + expect(element.querySelector(".card__source-text")?.textContent).toContain("Проект А"); + expect(element.querySelector(".card__title")?.textContent?.trim()).toBe("Frontend-разработчик"); + expect(element.querySelector(".card__description")?.textContent).toContain( + "Создание интерфейсов", + ); + expect( + Array.from(element.querySelectorAll(".card__skill")).map(node => node.textContent?.trim()), + ).toEqual(["Angular", "TypeScript"]); + expect(element.querySelector(".card__industry")?.textContent?.trim()).toBe("IT"); + expect(element.querySelector(".card__cta")?.textContent).toContain("Подробнее о вакансии"); + expect(element.querySelector("a.card__cta")?.getAttribute("href")).toBe("/office/vacancies/7"); + expect(element.querySelector("time")?.getAttribute("datetime")).toBe("2026-09-23T12:00:00Z"); + + fixture.componentRef.setInput("feedItem", { + ...fixture.componentInstance.feedItem(), + requiredSkills: ["QA", "Postman", "API", "SQL", "Git", "Аналитика"].map((name, id) => ({ + id, + name, + })), + }); + fixture.detectChanges(); + expect( + Array.from(element.querySelectorAll(".card__skills li")).map(node => + node.textContent?.trim(), + ), + ).toEqual(["QA", "Postman", "API", "+3"]); + expect(element.querySelector(".card__skills li:last-child")?.getAttribute("aria-label")).toBe( + "Ещё навыков: 3", + ); + expect(fixture.componentInstance.feedItem().requiredSkills).toHaveLength(6); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.ts b/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.ts index b8210a370..512e4bb9d 100644 --- a/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.ts +++ b/projects/social_platform/src/app/ui/pages/feed/open-vacancy/open-vacancy.component.ts @@ -1,77 +1,25 @@ /** @format */ -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - ElementRef, - inject, - input, - Input, - viewChild, - ViewChild, -} from "@angular/core"; -import { CommonModule } from "@angular/common"; -import { ButtonComponent } from "@ui/primitives"; -import { Router, RouterLink } from "@angular/router"; -import { TagComponent } from "@ui/primitives/tag/tag.component"; +import { ChangeDetectionStrategy, Component, inject, input } from "@angular/core"; +import { RouterLink } from "@angular/router"; +import { DayjsPipe, ParseBreaksPipe, ParseLinksPipe } from "@corelib"; import { Vacancy } from "@domain/vacancy/vacancy.model"; -import { AvatarComponent } from "@ui/primitives/avatar/avatar.component"; -import { TruncatePipe, DayjsPipe, ParseBreaksPipe, ParseLinksPipe } from "@corelib"; import { AppRoutes } from "@api/paths/app-routes"; import { IndustryRepositoryPort } from "@domain/industry/ports/industry.repository.port"; -import { ExpandService } from "@api/expand/expand.service"; +import { AvatarComponent } from "@ui/primitives/avatar/avatar.component"; -/** Карточка вакансии в ленте с поддержкой разворачивания контента. */ +/** Карточка вакансии в ленте: источник, роль, описание, навыки и переход к вакансии. */ @Component({ selector: "app-open-vacancy", - imports: [ - CommonModule, - ButtonComponent, - RouterLink, - TagComponent, - DayjsPipe, - ParseLinksPipe, - ParseBreaksPipe, - TruncatePipe, - AvatarComponent, - ], + imports: [RouterLink, DayjsPipe, ParseLinksPipe, ParseBreaksPipe, AvatarComponent], templateUrl: "./open-vacancy.component.html", styleUrl: "./open-vacancy.component.scss", changeDetection: ChangeDetectionStrategy.OnPush, - providers: [ExpandService], }) -export class OpenVacancyComponent implements AfterViewInit { +export class OpenVacancyComponent { readonly feedItem = input.required(); + readonly publishedAt = input(""); - private readonly expandService = inject(ExpandService); protected readonly AppRoutes = AppRoutes; - private readonly industryRepository = inject(IndustryRepositoryPort); - - private descEl = viewChild("descEl"); - - readFullSkills = false; - - protected readonly descriptionExpandable = this.expandService.descriptionExpandable; - protected readonly readFullDescription = this.expandService.readFullDescription; - protected readonly industryRepositoryGetOne = (id: number) => this.industryRepository.getOne(id); - - ngAfterViewInit(): void { - setTimeout(() => { - this.expandService.checkExpandable("description", true, this.descEl()); - }); - } - - protected onExpandDescription(elem: HTMLElement): void { - this.expandService.onExpand( - "description", - elem, - "expanded", - this.expandService.readFullDescription(), - ); - } - - protected toggleSkills(): void { - this.readFullSkills = !this.readFullSkills; - } + protected readonly industryRepository = inject(IndustryRepositoryPort); } diff --git a/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.html b/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.html index 7774f3f65..9d6772d95 100644 --- a/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.html +++ b/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.html @@ -32,17 +32,26 @@
@for (filterItem of feedFilterOptions; track $index) { -
-
+
-

{{ filterItem.name }}

-
+ + {{ filterItem.name }} + + {{ categoryCount(filterItem.value) }} + + }
diff --git a/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.scss b/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.scss index 640a68b85..e01406285 100644 --- a/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.scss +++ b/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.scss @@ -1,3 +1,5 @@ +/** @format */ + @use "styles/typography"; @use "styles/responsive"; @@ -74,20 +76,25 @@ } .filter { - display: flex; - gap: 20px; - align-items: center; - justify-content: space-between; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 30px 12px; + padding-top: 25px; + + @include responsive.apply-desktop { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } &__option { position: relative; - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: 32px 18px; + row-gap: 4px; align-items: center; - justify-content: center; - width: 156px; - height: 66px; - padding: 40px 24px 12px; + justify-items: center; + min-width: 0; + height: 104px; + padding: 36px 6px 12px; cursor: pointer; background-color: var(--light-white); border: 0.5px solid var(--medium-grey-for-outline); @@ -99,6 +106,8 @@ } &--active { + border-color: var(--accent); + i { color: var(--light-white) !important; } @@ -106,6 +115,10 @@ .filter__option--icon { background-color: var(--accent); } + + .filter__title { + color: var(--accent); + } } &--icon { @@ -127,16 +140,27 @@ } } - p { - color: var(--grey-for-text); - text-align: center; + &:focus-visible { + outline: 2px solid var(--accent-dark); + outline-offset: 3px; } } - span { + // Одна двухстрочная область названия задаёт общую базовую линию всех счётчиков. + &__title { + font-size: 11px; + line-height: 16px; + color: var(--grey-for-text); + text-align: center; + } + + &__count { display: block; - margin-bottom: 6px; - color: var(--gray); + font-size: 14px; + font-weight: 600; + font-variant-numeric: tabular-nums; + line-height: 18px; + color: var(--accent); } &__checkbox { diff --git a/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.spec.ts b/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.spec.ts index 7951f824e..eecc17119 100644 --- a/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.spec.ts +++ b/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.spec.ts @@ -5,6 +5,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { FeedFilterComponent } from "./feed-filter.component"; import { RouterTestingModule } from "@angular/router/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; +import { FeedUIInfoService } from "@api/feed/facades/ui/feed-ui-info.service"; describe("FeedComponent", () => { let component: FeedFilterComponent; @@ -13,6 +14,7 @@ describe("FeedComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [FeedFilterComponent, RouterTestingModule, HttpClientTestingModule], + providers: [FeedUIInfoService], }).compileComponents(); fixture = TestBed.createComponent(FeedFilterComponent); @@ -22,4 +24,40 @@ describe("FeedComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); + + it("показывает счётчики всех шести категорий независимо от фильтра", () => { + TestBed.inject(FeedUIInfoService).categoryCounts.set({ + all: 12, + project: 5, + vacancy: 3, + news: 4, + partnerprogram: 0, + education: 0, + }); + fixture.detectChanges(); + + const options = Array.from( + fixture.nativeElement.querySelectorAll(".desktop .filter__option"), + ) as HTMLElement[]; + expect(options).toHaveLength(6); + expect( + options.map(option => option.querySelector(".filter__count")?.textContent?.trim()), + ).toEqual(["12", "5", "3", "4", "0", "0"]); + for (const option of options) { + expect(option.tagName).toBe("BUTTON"); + expect(Array.from(option.children).map(child => child.className)).toEqual([ + "filter__option--icon", + "filter__title", + "filter__count", + ]); + expect(option.querySelector(".filter__option--icon")?.getAttribute("aria-hidden")).toBe( + "true", + ); + } + expect((options[5] as HTMLButtonElement).disabled).toBe(true); + expect(options[0].getAttribute("aria-pressed")).toBe("true"); + const setFilter = vi.spyOn(component, "setFilter"); + options[1].click(); + expect(setFilter).toHaveBeenCalledExactlyOnceWith("project"); + }); }); diff --git a/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.ts b/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.ts index 4294faca1..464fcb042 100644 --- a/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.ts +++ b/projects/social_platform/src/app/ui/widgets/feed-filter/feed-filter.component.ts @@ -9,6 +9,8 @@ import { feedFilter } from "@core/consts/filters/feed-filter.const"; import { FeedFilterInfoService } from "./service/feed-filter-info.service"; import { DetailProfileInfoService } from "../detail/services/profile/detail-profile-info.service"; import { ProfileDetailUIInfoService } from "@api/profile/facades/detail/ui/profile-detail-ui-info.service"; +import { FeedUIInfoService } from "@api/feed/facades/ui/feed-ui-info.service"; +import { FeedCategoryCounts } from "@domain/feed/feed-item.model"; /** Компонент фильтрации ленты по типам контента с мгновенной синхронизацией через URL. */ @Component({ @@ -30,6 +32,7 @@ import { ProfileDetailUIInfoService } from "@api/profile/facades/detail/ui/profi }) export class FeedFilterComponent implements OnInit { private readonly feedFilterInfoService = inject(FeedFilterInfoService); + private readonly feedUIInfoService = inject(FeedUIInfoService); // Состояние выпадающего меню фильтров protected readonly filterOpen = this.feedFilterInfoService.filterOpen; @@ -38,6 +41,11 @@ export class FeedFilterComponent implements OnInit { protected readonly includedFilters = this.feedFilterInfoService.includedFilters; protected readonly feedFilterOptions = feedFilter; + protected readonly categoryCounts = this.feedUIInfoService.categoryCounts; + + protected categoryCount(value: string): number { + return this.categoryCounts()[(value || "all") as keyof FeedCategoryCounts] ?? 0; + } ngOnInit() { this.feedFilterInfoService.initializationFeedFilter(); diff --git a/projects/social_platform/src/app/ui/widgets/news-card/carousel/carousel.component.html b/projects/social_platform/src/app/ui/widgets/news-card/carousel/carousel.component.html index ceb82d7c5..36f13f525 100644 --- a/projects/social_platform/src/app/ui/widgets/news-card/carousel/carousel.component.html +++ b/projects/social_platform/src/app/ui/widgets/news-card/carousel/carousel.component.html @@ -1,16 +1,26 @@ @if (images().length) { -