feat(ai-chat): auto-collapse chat window on page focus (#42) #50

Merged
Ghost merged 30 commits from feat/ai-chat-collapse-on-focus into develop 2026-06-21 01:36:54 +03:00

Реализация #42 — авто-сворачивание окна AI-чата в заголовок при фокусе на странице. Ветка от develop. Closes #42.

Поведение

Окно само сворачивается в заголовок (визуальный коллапс, НЕ закрытие — ChatThread остаётся смонтирован, поток ответа не прерывается), как только пользователь кликает по странице вне окна; разворачивается по клику на заголовок.

Что сделано (5 из 6 частей плана — 6-я, minimized-CSS, уже была)

  1. Слушатель mousedown на document в capture-фазе, активен только при windowOpen && !minimized; сворачивает при pointer-down вне окна. Гарды: клики внутри окна и внутри Mantine [data-portal] (кебаб-меню списка чатов + confirm-модалка удаления) игнорируются — иначе клик по «Rename»/«Delete» свернул бы чат.
  2. Разворот по клику на заголовок: startDrag различает клик/драг по порогу 4px (minimizedRef против stale-closure); клик-разворот не сохраняет геометрию.
  3. setMinimized(false) при открытии окна (свёрнутое состояние не залипает между сессиями).
  4. Доступность: в свёрнутом виде заголовок — клавиатурный триггер разворота (role=button, tabIndex, aria-label=Expand, Enter/Space). По ревью вынес роль на сам заголовок, НЕ на контейнер dragBar (иначе role=button оборачивал бы вложенные кнопки Minimize/Close — невалидный ARIA).
  5. Чистые хелперы shouldCollapseOnOutsidePointer + isHeaderClick с unit-тестами (vitest, 9 кейсов).

Проверено на ревью

APPROVE. Ключевые риски закрыты: открытие окна НЕ само-сворачивается (открывающий mousedown происходит до монтирования слушателя; autofocus композера — это focus, не mousedown); слушатель снимается корректно (тот же ref + capture-флаг); portal-guard покрывает и shared-портал. Поймали невалидный ARIA (вложенные кнопки под role=button) → исправил.

Проверка

tsc --noEmit — exit 0. vitest — 9/9. (pnpm --filter client lint сломан в окружении — pre-existing, fallback на tsc.)

🤖 Generated with Claude Code

Реализация #42 — авто-сворачивание окна AI-чата в заголовок при фокусе на странице. Ветка от develop. Closes #42. ## Поведение Окно само сворачивается в заголовок (визуальный коллапс, НЕ закрытие — `ChatThread` остаётся смонтирован, поток ответа не прерывается), как только пользователь кликает по странице вне окна; разворачивается по клику на заголовок. ## Что сделано (5 из 6 частей плана — 6-я, minimized-CSS, уже была) 1. Слушатель `mousedown` на `document` в **capture-фазе**, активен только при `windowOpen && !minimized`; сворачивает при pointer-down вне окна. Гарды: клики внутри окна и внутри Mantine `[data-portal]` (кебаб-меню списка чатов + confirm-модалка удаления) игнорируются — иначе клик по «Rename»/«Delete» свернул бы чат. 2. Разворот по клику на заголовок: `startDrag` различает клик/драг по порогу 4px (`minimizedRef` против stale-closure); клик-разворот не сохраняет геометрию. 3. `setMinimized(false)` при открытии окна (свёрнутое состояние не залипает между сессиями). 4. Доступность: в свёрнутом виде заголовок — клавиатурный триггер разворота (`role=button`, `tabIndex`, `aria-label=Expand`, Enter/Space). По ревью вынес роль на сам заголовок, НЕ на контейнер dragBar (иначе `role=button` оборачивал бы вложенные кнопки Minimize/Close — невалидный ARIA). 5. Чистые хелперы `shouldCollapseOnOutsidePointer` + `isHeaderClick` с unit-тестами (vitest, 9 кейсов). ## Проверено на ревью APPROVE. Ключевые риски закрыты: открытие окна НЕ само-сворачивается (открывающий mousedown происходит до монтирования слушателя; autofocus композера — это focus, не mousedown); слушатель снимается корректно (тот же ref + capture-флаг); portal-guard покрывает и shared-портал. Поймали невалидный ARIA (вложенные кнопки под role=button) → исправил. ## Проверка `tsc --noEmit` — exit 0. vitest — 9/9. (`pnpm --filter client lint` сломан в окружении — pre-existing, fallback на tsc.) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Ghost added 1 commit 2026-06-20 23:46:12 +03:00
The floating chat window covered page content; you could only collapse it
manually. Now it auto-collapses to its header (visual collapse only — ChatThread
stays mounted so an in-flight stream isn't interrupted) when you interact with
the page, and expands again from the header.

- document mousedown listener in the CAPTURE phase, armed only when
  windowOpen && !minimized; collapses on a pointer-down outside the window.
  Guards: ignore clicks inside the window and inside any Mantine [data-portal]
  (the chat-list kebab menu + delete-confirm modal render in portals).
- Header click expands: startDrag distinguishes click vs drag by a 4px
  threshold (minimizedRef avoids a stale closure); an expand-click doesn't
  persist geometry.
- Reset minimized=false when the window opens (no sticky collapsed state).
- a11y: when minimized, the title is the keyboard expand affordance
  (role=button, tabIndex, aria-label Expand, Enter/Space) — kept off the
  dragBar container so no role=button wraps the Minimize/Close buttons.
- Pure helpers shouldCollapseOnOutsidePointer + isHeaderClick with vitest tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ghost merged commit d105397dcf into develop 2026-06-21 01:36:54 +03:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#50