# VenueFlow R2 — #507 / #530 Готовый локальный HTML/CSS/JS-макет и handoff для Cursor. Вход: **index.html**. Это дизайн с синтетическими состояниями, не реализация в приложении и не данные DEV. Исходники приложения, Git, runtime, БД и прежние свидетельства #507/#521 не изменялись. PROD запрещён, C5 OFF. ## Открыть и проверить Из этой папки: ```sh python3 -m http.server 8765 --bind 127.0.0.1 ``` Открыть `http://127.0.0.1:8765/index.html`. Только localhost; зависимости не устанавливаются. CSP разрешает локальные шрифт/изображения/скрипты и запрещает `connect-src` и отправку форм. Нет fetch, API-записей, cookies, localStorage или удалённых шрифтов. Внешние ссылки в демонстрации перехвачены; показан источник/контакт, переход или звонок не выполняется. Панель оператора **за пределами продукта** выбирает экран, состояние, настоящий viewport iframe и исход локальной операции. Все примеры помечены «Пример макета; не данные DEV». Схематичный фон карты отдельно назван схемой. Единственная фотография — существующее официальное фото Unischool, не пользовательское вложение. Примеры ссылок: - `?surface=page&state=empty&width=1280` - `?surface=page&state=own&width=390&height=844§ion=ratings` - `?surface=map&state=empty&width=390&height=844` - `?surface=map-expanded&state=populated&width=1280&height=900` - `?surface=picker&state=populated&width=390&height=844` - `?surface=composer&state=consent&width=390&height=844` `surface`: `page`, `map`, `map-expanded`, `picker`, `results`, `composer`. `state`: см. меню и manifest. Пустой авторизованный сценарий — по умолчанию; заполнение всегда пример, не свидетельство реальных оценок. Сценарии применяются к соответствующему блоку; `consent/submitted/private/eligible/ineligible/expired` нужны с `results/composer`. `reviews-*` — с `page/map-expanded`. `section`: `ratings`, `discussion`, `reviews`; `width=auto` следует реальной ширине окна. `height` необязателен: без него страница растёт по содержимому, карта занимает 900 px. Для проверки fixed bottom nav/внутренней прокрутки задать `height=844` или `900`. При ширине 820 действует мобильный лист, при 1024+ — немодальный инспектор 450 px. API в верхнем окне: ```js await window.setScenario({surface:'map-expanded', state:'own', width:390, height:844}); const metrics = await window.measureScenario(); // metrics: viewport, document, horizontalOverflow, controls, smallControls, // avatars, onestLoaded, images, activeElement, dialogOpen, inspectorWidth. window.setFixtureOutcome('unknown'); // success | failed | unknown; не сбрасывает состояние window.setFixtureOutcome('success'); // затем нажать «Повторить тот же запрос» window.getFixtureState(); // только синтетическая модель, без сессии/секретов window.expireFixtureSession(); // демонстрация очистки приватного UI ``` `setScenario` сбрасывает локальный пример и ждёт шрифт, изображения и два animation frame. `measureScenario` также дожидается их; возвращает реальные размеры, а не утверждение PASS. Переключение ширины **через меню** и обычный resize сохраняют выбранную площадку, черновик и операцию; смена сценария — явный reset фикстуры. Переключатель исхода операции не сбрасывает UNKNOWN. Для изменения размеров через браузер без reset можно менять ширину `#preview` либо размер окна при `width=auto`. Для Playwright: продукт находится в `page.frameLocator('#preview')`; API доступен в родительском `page.evaluate`. Сначала дождаться `await window.measureScenario()`. Скриншоты продуктового фрейма сопровождать внешней подписью примера; screenshot всей workbench уже её содержит. `screenshot-plan` в manifest содержит **намерения, не созданные изображения**. Не нужно проходить декартово произведение всех меню. ## Принятая композиция - Полная страница: название и физический адрес, официальное фото/профиль, расписание, **Оценки сообщества → Обсуждение площадки → Отзывы после игры**. Контакты в правой колонке, на мобильном доступны в обычном потоке. Пустое расписание не отключает сообщество. - Карта: компактный инспектор/превью с явными `Оценить`, `Обсуждение`, `Отзывы после игры`; они раскрывают тот же выбранный объект внутри карты. Расширенное представление использует те же `rating()`, `discussion()`, `reviews()`. Никакой обязательной навигации на полную страницу. Старое решение #507 «сводка + ссылка на страницу» **заменено** последним Owner требованием. - Пять звёзд сохраняют исходную шкалу 1–5. Это native radios в hit area 48×48; заполнение до выбранной звезды, лёгкий круг только у выбранной, текст `4 из 5 · Хорошо`. Нет плиток, числового нового рейтинга, автоматической отправки при выборе или нулевого агрегата. Arrow/Home/End меняют и значение, и фокус; сохранение отдельно. - Один card boundary/title для обсуждения. В инспекторе разделители вместо вложенных карточек; 16–20 px inset и меньшие заголовки, но кнопки не мельче 44 px. Страница: 20/26 px inset, 18 px radius. Onest, ink/paper/lime/purple из Guide. - Жалоба — короткое `Пожаловаться` в ghost Button, точный target в accessible name. На 390 действие переносится под заголовок. Меню комментария содержит свои `Редактировать / Удалить` либо чужое `Пожаловаться`; сам текст остаётся на месте после жалобы. - Аватар 40×40, ответ 32×32, `overflow:hidden`, img 100%/`object-fit:cover`. Отсутствие/ошибка — те же инициалы и геометрия. В `long` официальное фото зала используется **только как явно обозначенная регрессионная avatar-фикстура**. В `noavatar` искусственно испускается событие `error` на локальном img и вызывается обычный fallback; нет запроса битого URL/404. Это не проверка реального сбоя CDN. - Публичные отзывы только читаются. Нет composer, login CTA, списка игр, профиля автора или информации об игре. Отдельную новую публичную UI жалобу на verified review этот макет не добавляет; существующий отдельный API не меняется. ## Поведение и пределы демонстрации Работают локально: звёзды/save/change/withdraw; pending/error/UNKNOWN; комментарий, ответ, own edit/delete, read more, сортировка; меню, жалоба и её receipt; поиск/выбор площадки и отдельная кнопка фото; login/cancel/return к разделу без автосабмита; фиксированный composer, выбор audience, сброс согласия при изменении текста/темы, private/public receipt. Сводка после голоса меняется по заранее заданным fixture-ответам, **не по production-клиентскому расчёту среднего**. Три примера оценки — малая выборка, без убедительности «рейтинга места». Статические `pending/withdraw` предназначены для осмотра зависшего состояния. Нормальный клик показывает pending на 650 ms и затем выбранный оператором исход. UNKNOWN сохраняет синтетический immutable request в памяти, не показывает успех, не делает автоматического повтора. Локальные переключения map/page не создают вторую модель. Пока операция не завершена, конфликтующая отправка блокируется. Это **демонстрация**, не замена useSocialMutation, серверной идемпотентности, version-conflict или реального auth chooser. Удаление комментария при error/UNKNOWN показывает неподтверждённый результат, без фальшивого tombstone; полноценное восстановление receipt удаления остаётся действующему handler приложения. Нативный `` даёт модальный focus trap/Escape. Закрытие возвращает фокус на trigger, после перерисовки — на его `data-focus` аналог. Меню дополнительно поддерживает Arrow/Home/End. Escape закрывает диалог раньше инспектора. Desktop map не получает modal trap; zoom/pan схемы остаются снаружи. Mobile: вертикальный жест за handle/неинтерактивную часть компактной карточки/row button, исключение форм и action controls; горизонтальный swipe официального фото не меняет высоту и подавляет только click после swipe. Фото одно: второй не выдуман. Expanded content имеет один scroll owner, жест по контенту его не сворачивает. Сохранены 1024 breakpoint, bottom nav, safe-area и reduced-motion. Native mobile keyboard/inert lifecycle и настоящий MapLibre/gallery continuity проверяются в приложении, не объявляются проверенными здесь. ## Cursor: точный frontend handoff Все пути ниже относительно `apps/web/src/`, source **24889954ba32c6d6f25b2ebfe059817babf6da19**. Копировать визуальные решения, не переносить демонстрационный DOM renderer или fixture auth в приложение. | Место | Изменение и сохранённый контракт | |---|---| | `components/consumer/venues/VenuePage.tsx` | Полная последовательность блоков, одна оболочка discussion; сохранить venue media/profile/schedule. Удалить обещание I2. | | `components/consumer/venues/VenueCommunityRating.tsx`, `styles/venue-community.css` | Звёзды-radio, явное сохранение, ghost report, own errors. **Version 0 + value null → upsert**, не update; состояние после withdrawal тоже проверять по `allowedActions`. Own GET failure не выдавать за разрешённый пустой голос. | | `components/consumer/comments/CommentsSection.tsx`, `styles/comments.css` | Presentation props для embedded/density/title; общие root/reply handlers и версии. Ограничить `.gb-comments .avatar`/img локально, оставить большие профильные варианты. Own replies получают edit/delete при `mine && canCreate`. Не добавлять вторую реализацию comments. | | `components/consumer/players/ProfileComposition.tsx` | Повторно использовать Avatar/ProfileDialog/AuthChooser. Не оборачивать страницу в `.fr3`; её отсутствие — причина утечки avatar/dialog skin. | | `components/consumer/venues/VenueCommunityReportDialog.tsx`, `components/consumer/Button.tsx`, `components/consumer/CloseButton.tsx` | Community opt-in существующего ProfileDialog/полей, close в header, один scroll body, labels над полями; точные target/version/reason и useSocialMutation без изменения. Существующий путь: `components/consumer/CloseButton.tsx`; новый CloseButton не создавать. | | `components/consumer/games/GamesMapView.tsx`, `VenuePopup.tsx`, `VenueSheet.tsx`, `VenuePreviewCard.tsx`, `DesktopMapInspector.tsx` | Одна общая business-state ownership на selected venue, два density/view. Превью-actions **siblings** row button. Раскрытие/сворачивание/resize не размонтируют unresolved operation. Подключить current Me и allowlisted venue+section auth return в существующей навигации; не класть payload/auth в URL. Batched public summary остаётся, own reads только selected venue. | | `components/consumer/games/VenuePickerDialog.tsx`, `styles/venue-gallery.css`, существующие `styles/games.css` | Общий padded hover/focus row; plain summary внутри selection button в title column, отдельные community/photo sibling controls. Выбор не срабатывает по фото/community. | | `components/consumer/venues/VenueReviewsPanel.tsx`, `VenueReviewsDialog.tsx` | Read-only, `Отзывы после игры`, loading/error/empty/published. Удалить public writer/login CTA, сохранить cursor pagination. | | `components/consumer/games/GameResultsComposition.tsx`, `GameFeedbackPanel.tsx`, `components/consumer/venues/VenueReviewComposer.tsx` | Private entry только для exact returned gameId и `admission=may_submit`. Required fixed gameId, plain context вместо dropdown, никакого `eligible[0]` fallback. Recheck before submit, expected outcome/attendance versions. Существующий PostgameReminders не менять в новый review scheduler. | | `lib/use-venue-community.ts`, `lib/venue-community-api.ts`, `lib/comments-api.ts`, `components/consumer/messages/useSocialMutation.ts`, `lib/venue-reviews-api.ts` | Переиспользовать, не создавать registry/service. Сохранить ограничения, CSRF/session, immutable payload/key, version conflict, invalidation и fence поздних ответов. | D1–D5 относятся к исходному PR #528 (Refs #507), reported DEV `19018791` / alpha.58. Дословные маршруты/Seen/Expected и прежние PASS/NOT_TESTED остаются в `inputs/visual530-findings.md`; не перезаписывать их. Исходный макет: `https://files.anpavel.ru/projects/gobadminton/design/style-guide/venue-community/20261107-astra-v1/`. Сеть не использовалась. Локальные reference inputs и frozen Guide прочитаны. | Finding | Решение в R2 | |---|---| | D1 `/games/create`, 1280/390 | Summary внутри общей padded selection row, под названием; coherent group hover, фото отдельной компактной sibling action. | | D2 venue rating, 390 | Заголовок сохраняет ширину, report переносится внутри card. | | D3 venue rating, 1280/390 | Ghost Button вместо browser-default button. | | D4 report dialog, 1280/390 | ProfileDialog skin opt-in, labels сверху, X в header, обычные Guide поля. | | D5 sheet/preview, 390 | Явные 44+ px direct map actions. Прежний link-only путь отменён Owner. | Community report: `POST /v1/reports`, `targetKind=venue_community`; `aggregate` без версии, `own_vote` с expectedVersion, `venue_comment` с commentId/expectedVersion. Причины без изменения: `harassment`, `spam`, `fake_profile`, `inappropriate`, `other`. Detail ≤1000. Receipt = submitted, не скрытие/решение по жалобе. Отдельный verified-review reports API не смешивать с community. ## Private postgame и зависимости проекций `GET /v1/me/venue-reviews/eligibility?venueId=…`: показывать entry только для exact held game с `may_submit`; `hold`, `deny`, `missing_history`, absent/error/loading/session-loss скрывают CTA. `window_closed` имеет отдельную честную копию; отсутствие basis не значит expired. Mutable game venue — lookup candidate, не доказательство исторического места игры. Latest-30 ограничения API не обходить другим game. Composer по умолчанию `self_draft`. `public_venue` показывает exact projected authorLabel/text/topics/canonical venue и **неотмеченное** согласие. Сохранён точный текущий текст: «Подтверждаю публикацию этого текста без моего профиля, игры и состава.» Изменение text/topic/venue/session сбрасывает consent; поздний preview response не может его вернуть. Макет локально моделирует projection, production получает consentProjectionHash от существующего consent-preview. Текст ≤500. Receipt `self_draft` отдельно от `submitted_waiting/publicVisible=false`. Cases и существующая задержка публикации сохраняются; даты публикации не обещаются. | Неопределённое поле | Безопасное решение в этом срезе | |---|---| | Effective deadline / appeal pause отсутствуют в DTO | Никаких дат/таймера. Существующие 14 дней **от first eligibility с appeal pause**, никогда game end +14. `notBefore` — не submission deadline. | | Own review → game association отсутствует | Только receipt текущей точно связанной операции; не выдавать persisted per-game status по совпавшему venueId. | | Durable own-comment moderation state отсутствует | Нет выдуманных withheld/pending badges и гарантии публичности собственного текста. | | Public profanity display metadata ещё не зафиксированы | `masked` — условный серверный display fixture «Покрытие ***** скользкое, нужна уборка.» + тихое «Часть выражений скрыта в публичном тексте». Показ notice **только по точному server mask flag**. Обычный comment без notice; нет raw reveal, бана/страйка или ложного moderation result. Own edit использует существующий контракт. Backend field names/escaping/root audit — отдельная зависимость; не внедрять клиентский фильтр. | | Map auth return и shared ownership ещё не wired | Bounded frontend wiring в существующих host/navigation. Нет новых сервисов и per-marker private reads. | ## Provenance и проверка Guide: `/opt/gobadminton-clean-qa/style-guide`, `src/App.tsx` SHA256 `04187d4d7d70c8c61e9ca2f08b6b6325a98539a3ec90c711c48ab771f675b61c`, CSS `f660e31973a6142784308f22728c751d7bb7432fb2ac0136bac032dbc6b1db2a` — сверены в этом запуске. Использованы SectionHeader/SurfacePanel/ListRow/Choice/Button/Modal/Notice и карта Radar; это source parity, не rendered parity. `STYLE_GUIDE_GAP`: нет готовой полной venue community композиции. `OWNER_VISUAL_OVERRIDE`: star tiles и map-link-only заменены; сохраняется исходная пятизвёздочная семантика. Артефакт финального #521 здесь не предоставлен: точное соответствие его overlay spacing не подтверждено; архитектуру overlay не пересобирать. `assets/onest.ttf`: действительный TTF, декодированный предоставленный approved old asset, hash `3faa4b905661849b2332e394b42f91b5bf5575e553c516caa81811e868a4d589`; provenance в inputs. Фото/credit/venue identity — `assets/official-venue-public.json`; credit **«Фото: Golden Shuttle — зал Unischool»**, `https://goldenshuttle.ru/`. В metadata площадка `dataStatus/lifecycleState=unverified`: бейдж «проверенная площадка» не добавлен. Logo/icons — существующие SVG Guide, нового логотипа нет. Личные имена, текст и avatar из приватных скриншотов в output не копировались. Выполнено: синтаксическая проверка `app.js/workbench.js` Node 24; проверка локальных asset references и manifest. Chromium/Firefox в PATH этого ограниченного окружения отсутствуют. **Browser screenshots, overflow/touch/focus measurements, rendered visual QA, independent QA: NOT_RUN. Owner UAT: NOT_TESTED.** Функции измерения готовы для родителя; ни screenshot, ни browser PASS не выдуманы. Это законченный design artifact, не DONE приложения: CI/независимый QA/DEV ACCEPTED/smoke и release history остаются существующему процессу.