From e7b7f48d3522113efd5b065bc636c2e991890262 Mon Sep 17 00:00:00 2001 From: vvzvlad Date: Thu, 18 Jun 2026 20:41:17 +0300 Subject: [PATCH] docs(backlog): add AI chat collapse and comment density docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two new backlog documentation files: - ai-chat-collapse-on-page-focus.md describing auto‑collapse behavior for the AI chat window. - comments-panel-density.md outlining UI density improvements for the comments panel. --- .../backlog/ai-chat-collapse-on-page-focus.md | 263 ++++++++++++++++++ docs/backlog/comments-panel-density.md | 181 ++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 docs/backlog/ai-chat-collapse-on-page-focus.md create mode 100644 docs/backlog/comments-panel-density.md diff --git a/docs/backlog/ai-chat-collapse-on-page-focus.md b/docs/backlog/ai-chat-collapse-on-page-focus.md new file mode 100644 index 00000000..fd134684 --- /dev/null +++ b/docs/backlog/ai-chat-collapse-on-page-focus.md @@ -0,0 +1,263 @@ +# Авто-сворачивание AI-чата в заголовок при фокусе на странице, разворот по клику + +## Контекст (запрос) + +Плавающее окно AI-чата (`AiChatWindow`) сейчас перекрывает контент страницы: +если открыть чат и начать читать/листать вики-страницу под ним, окно остаётся +во весь рост и закрывает таблицу/текст (см. скриншот: окно поверх «Аудио-тракт в +умных колонках»). Свернуть можно только вручную — кнопкой «—» (Minimize) в шапке. + +Хотим, чтобы окно **само сворачивалось в свою шапку, как только пользователь +переключается на страницу** (кликает мимо окна — в редактор/в контент), и +**разворачивалось обратно по клику на шапку**. Тогда чат не мешает читать +страницу, но остаётся под рукой одним кликом. + +Важно: сворачивание — это именно визуальный коллапс (как нынешний Minimize), а +**не** закрытие. Поток ответа агента не должен прерываться. + +## Как сейчас устроено (цепочка) + +Всё во фронтенде, в одном компоненте окна: +`apps/client/src/features/ai-chat/components/ai-chat-window.tsx` +(+ его CSS `ai-chat-window.module.css`). + +- **Состояние «свёрнуто»** уже есть: `const [minimized, setMinimized] = useState(false)` + — строка ~108. +- **Переключатель** `toggleMinimize` (строки ~319-321) просто инвертирует флаг; + привязан к кнопке «—» (`IconMinus`) в шапке (строки ~366-374). +- **Визуальный коллапс уже реализован в CSS** (`ai-chat-window.module.css`): + - `.minimized { height: auto !important; min-height: 0 !important; resize: none; }` + (строки ~40-44) — окно схлопывается до высоты шапки; + - `.minimized .content { display: none; }` (строки ~56-58) — тело + (история + тред) скрывается, но **не размонтируется**: `ChatThread` остаётся + в DOM, поэтому идущий стрим/`AbortController` не обрывается (это явно описано в + комментариях у `.content` и в `toggleMinimize`). + - При `minimized` инлайновая `height` не задаётся (строка ~334), чтобы победила + auto-высота из CSS; резайз-ручка скрыта (строки ~454-458). +- **Шапка = `.dragBar`** (JSX строки ~338-385) с `onMouseDown={startDrag}`. + - `startDrag` (строки ~262-314) игнорирует нажатия на кнопках + (`if ((e.target).closest("button")) return;`, строка ~264) — чтобы «—»/«×»/«+» + не таскали окно. + - В `mouseup` (`up`, строки ~290-308) сохраняется итоговая позиция в `geom`. + - **Клика-для-разворота сейчас нет**: одиночный клик по шапке только инициирует + перетаскивание, развернуть свёрнутое окно можно лишь повторным нажатием «—». +- Окно смонтировано глобально и плавает над всем: `` в + `apps/client/src/components/layouts/global/global-app-shell.tsx` (строка ~159), + `position: fixed`, `z-index: 105` (ниже оверлеев Mantine: modal=200, menu=300, + notifications=400 — это нам важно, см. «Тонкие моменты»). +- Композер автофокусится при монтировании треда (`autoFocus` в + `chat-input.tsx`) — это фокус **внутри** окна, не на странице. + +Итого: «свёрнутый» вид готов. Нужно добавить **два триггера**: (1) авто-сворот при +взаимодействии со страницей и (2) разворот по клику на шапку. + +## Решение (точечное, только клиент) + +Файл: `apps/client/src/features/ai-chat/components/ai-chat-window.tsx` +(+ пара строк CSS, опционально + i18n-ключ). + +### Часть 1 — авто-сворачивание при взаимодействии со страницей + +Слушаем `mousedown`/`pointerdown` на `document` (в capture-фазе), но **только** +когда окно открыто и ещё не свёрнуто. Если нажатие пришло **вне окна** и **не +внутри портала Mantine** — сворачиваем. + +```ts +// Auto-collapse the window into its header as soon as the user interacts with +// anything outside it (clicks the page/editor). Active only while open and +// expanded. Capture phase so a child's stopPropagation can't hide the event. +useEffect(() => { + if (!windowOpen || minimized) return; + const onPointerDown = (e: MouseEvent) => { + const target = e.target as HTMLElement | null; + const el = winRef.current; + if (!el || !target) return; + // Inside the window itself -> not an "away" interaction. + if (el.contains(target)) return; + // Inside a Mantine portal the chat owns (kebab Menu dropdown, delete-confirm + // modal, the context-size Tooltip, notifications). Mantine's Portal sets + // data-portal="true" on its node, so this reliably excludes ALL of them. + if (target.closest("[data-portal]")) return; + setMinimized(true); + }; + document.addEventListener("mousedown", onPointerDown, true); + return () => document.removeEventListener("mousedown", onPointerDown, true); +}, [windowOpen, minimized]); +``` + +Почему `mousedown` (а не `focusin`): +- Клик по **не-фокусируемому** элементу страницы (ячейка таблицы, обычный текст — + ровно случай со скриншота) фокус-событие не порождает, но это и есть «ушёл на + страницу». `mousedown` ловит любой клик. `focusin` пропустил бы такие клики. +- Минус: `mousedown` не ловит переход фокуса с клавиатуры (Tab в редактор). Если + это нужно — добавить параллельно `focusin`-слушатель с тем же гардом (см. + «Открытые вопросы»). По умолчанию — только указатель, как и просит запрос + («смена фокуса на страницу» = клик мимо окна). + +Почему гард `[data-portal]` обязателен: +- Кебаб-меню списка чатов рендерит `Menu.Dropdown` в портал (вне DOM окна) — + `conversation-list.tsx` строки ~123-149; удаление — `modals.openConfirmModal` + (строка ~56), тоже портал. Без гарда клик по пункту «Rename»/«Delete» свернул + бы чат прямо в момент выбора. Mantine на узле портала ставит + `data-portal="true"` (подтверждено в `node_modules/@mantine/core` → + `Portal.cjs`), поэтому `target.closest("[data-portal]")` исключает их все + (а заодно Tooltip размера контекста и нотификации — они тоже порталы). + +Регистрация в `useEffect` с deps `[windowOpen, minimized]`: слушатель вешается +только когда `windowOpen && !minimized`, и снимается при сворачивании/закрытии — +не делаем лишней работы и не дёргаем `setMinimized(true)` повторно. + +### Часть 2 — разворот по клику на шапку + +Нужно отличить **клик** по шапке (развернуть) от **перетаскивания** свёрнутой +плашки (она остаётся таскаемой). Нельзя просто навесить `onClick` на `.dragBar`: +браузер шлёт `click` и в конце драга (mousedown+mouseup на том же элементе), и +плашка разворачивалась бы после любого перетаскивания. + +Решение — доработать существующий `startDrag`: запомнить стартовые координаты, +а в `mouseup` посчитать смещение; если оно ниже порога (≈4px) **и** окно сейчас +свёрнуто — развернуть. + +```ts +const startDrag = useCallback((e: React.MouseEvent): void => { + if ((e.target as HTMLElement).closest("button")) return; + const el = winRef.current; + if (!el) return; + const sx = e.clientX; + const sy = e.clientY; + // ... (ol/ot + move() unchanged) + + const up = (ev: MouseEvent): void => { + document.removeEventListener("mousemove", move); + document.removeEventListener("mouseup", up); + document.body.style.userSelect = ""; + // Treat a near-zero-movement press as a click. When minimized, a click on + // the header expands the window (drag still repositions the collapsed bar). + const moved = + Math.abs(ev.clientX - sx) > 4 || Math.abs(ev.clientY - sy) > 4; + if (!moved && minimizedRef.current) { + setMinimized(false); + return; // nothing to persist: position didn't change + } + // ... (persist geom as before) + }; + // ... +}, []); +``` + +Подводный камень — **stale closure**: `startDrag` обёрнут в `useCallback([])`, +поэтому замыкает устаревший `minimized`. Два варианта: +- держать `minimizedRef = useRef(minimized)` и синхронизировать его в эффекте + (`minimizedRef.current = minimized`) — тогда `useCallback([])` остаётся (как в + коде выше); **рекомендуется**, не пересоздаёт хендлер; +- либо добавить `minimized` в deps `useCallback` — проще, но пересоздаёт `startDrag` + на каждом тоггле (дёшево, но дёргает `onMouseDown`-проп). + +Кнопка «—» остаётся как явный тоггл (`toggleMinimize` уже инвертирует флаг), так +что развернуть можно и ей. Менять её не нужно. + +### Часть 3 (рекомендуется) — аффорданс и доступность шапки + +- **Курсор**: в свёрнутом виде шапка кликабельна — заменить `grab` на `pointer`: + ```css + /* ai-chat-window.module.css — hint that the collapsed header expands on click */ + .minimized .dragBar { cursor: pointer; } + ``` +- **Клавиатура/скринридер**: `.dragBar` — это `div`. В свёрнутом состоянии дать + ему `role="button"`, `tabIndex={0}`, `aria-label={t("Expand")}` и обработчик + Enter/Space → `setMinimized(false)`. Иначе развернуть без мыши нельзя. + +## Тонкие моменты / edge cases + +- **Стрим не прерывается.** Авто-сворот выставляет `minimized=true` — `ChatThread` + остаётся смонтированным (только `.content` скрывается). Ответ агента + достреливается в фоне; развернув шапку, пользователь видит результат. Это + желаемое поведение (он специально ушёл читать страницу). +- **Автофокус композера при открытии.** Открытие окна автофокусит textarea — + это `focus` **внутри** окна, а не внешний `mousedown`, поэтому ложного + немедленного сворота не будет. +- **Перетаскивание окна** (mousedown по шапке) — это нажатие **внутри** + `winRef.current`, гард `el.contains(target)` его пропускает: drag не сворачивает. +- **Резайз** нативной ручкой — mousedown тоже внутри окна, не сворачивает. +- **Порталы дочерних компонентов** (кебаб-меню, confirm-модалка, tooltip, + нотификации) исключены гардом `[data-portal]` — клик по ним не сворачивает. + Это ключевая причина не использовать «голый» contains-only outside-click. +- **Capture-фаза** слушателя: ловим `mousedown` даже если кто-то на странице + вызывает `stopPropagation` в bubble-фазе. На клики внутри окна/порталов не + влияет (их отсекают гарды). +- **Повторный авто-сворот** не происходит: при `minimized` слушатель снят (deps + эффекта). Разворот по клику снова навешивает слушатель — цикл корректен. +- **Состояние при закрытии/открытии.** Компонент при `!windowOpen` возвращает + `null`, но **не размонтируется**, поэтому `minimized` переживает закрытие. + Желательно при каждом открытии показывать окно **развёрнутым**: добавить + `setMinimized(false)` в эффект, срабатывающий на переход `windowOpen → true` + (можно в тот же `useLayoutEffect`, что вычисляет геометрию, строки ~238-241). + См. «Открытые вопросы». +- **z-index/оверлеи.** Окно (105) ниже modal/menu/notifications — поэтому + confirm-модалка удаления и кебаб-меню рисуются **над** окном; даже если бы чат + свернулся за ними, они продолжали бы работать. Но гард `[data-portal]` всё равно + не даёт сворачиваться при работе с ними. +- **Touch.** Драг сейчас на mouse-событиях (десктоп-фича). Для единообразия + внешний слушатель можно сделать `pointerdown` вместо `mousedown` (покроет тач), + но тогда и порог-клик в `up` стоит считать на pointer-событиях. По умолчанию — + `mousedown`, как у драга. + +## i18n + +- Новые пользовательские строки — **только через `t(...)`** и добавить ключ в + `apps/client/public/locales/en-US/translation.json` (каталог ключ==значение). + Достаточно `"Expand"` (для `aria-label`/`title` шапки в свёрнутом виде). + В шапке уже есть `t("Minimize")`, `t("Close")`, `t("New chat")`. +- Комментарии в коде — на английском (правило проекта). + +## Тесты + +- Вынести чистые хелперы и покрыть Vitest: + - `shouldCollapseOnOutsidePointer(target, windowEl): boolean` + (`windowEl.contains(target)` + `target.closest("[data-portal]")`) — + `(внутри окна) → false`, `(в портале) → false`, `(на странице) → true`. + - `isHeaderClick(dx, dy, threshold=4): boolean` — порог клик-vs-драг. +- Компонентный тест (`@testing-library/react`): открыть окно → диспатчить + `mousedown` по `document.body` → окно получает класс `.minimized`; клик по + `.dragBar` (без движения) в свёрнутом виде → класс снят. Проверить, что + `mousedown` по узлу с `data-portal` сворота не вызывает. +- Прогнать `pnpm --filter client lint` и `pnpm --filter client test`. + +## Файлы к изменению + +- `apps/client/src/features/ai-chat/components/ai-chat-window.tsx` + — внешний `mousedown`-эффект (Часть 1); доработка `startDrag` + `minimizedRef` + (Часть 2); опц. `setMinimized(false)` при открытии; a11y-атрибуты на `.dragBar`. +- `apps/client/src/features/ai-chat/components/ai-chat-window.module.css` + — опц. `.minimized .dragBar { cursor: pointer; }`. +- `apps/client/public/locales/en-US/translation.json` — ключ `"Expand"` (если + добавляем aria/title). + +## Альтернативы / расширения (вне базового объёма) + +- **`useClickOutside` из `@mantine/hooks`** вместо ручного слушателя. Минус: + порталы дочерних меню/модалок нужно явно передавать как `nodes` для игнора, а + они создаются динамически — ручной гард `[data-portal]` проще и надёжнее. + Поэтому ручной слушатель предпочтительнее. +- **Учитывать клавиатурный фокус** (`focusin`) дополнительно к `mousedown` — если + захотим сворачивать и при Tab в редактор. +- **Не сворачивать во время стрима** — если решим, что во время генерации окно + должно оставаться раскрытым (противоречит идее «ушёл читать страницу», поэтому + по умолчанию сворачиваем всегда). +- **Анимация коллапса/разворота** (height/opacity transition) — косметика, можно + добавить позже в `.window`/`.content`. + +## Принятые решения (базовый объём) + +- **Триггер авто-сворота — только клик** (`mousedown` в capture-фазе). + `focusin` не добавляем: запрос — про переключение на страницу кликом, а клик по + не-фокусируемому контенту (ячейка таблицы) фокус-событие не даёт. +- **При каждом открытии окна показываем его развёрнутым** — + `setMinimized(false)` на переход `windowOpen → true`. Свёрнутое состояние не + «залипает» между сессиями открытия. +- **Во время стрима сворачиваем как обычно.** Поток не прерывается (`ChatThread` + остаётся смонтированным), результат виден после разворота — это и есть смысл + «ушёл читать страницу». +- **Клавиатурный разворот шапки входит в базовый объём** — в свёрнутом виде + `.dragBar` получает `role="button"`, `tabIndex={0}`, `aria-label={t("Expand")}` + и обработку Enter/Space. Доступность без мыши обязательна. diff --git a/docs/backlog/comments-panel-density.md b/docs/backlog/comments-panel-density.md new file mode 100644 index 00000000..c17e569a --- /dev/null +++ b/docs/backlog/comments-panel-density.md @@ -0,0 +1,181 @@ +# Панель комментариев: сделать плотнее (меньше воздуха, меньше шрифт) + +Статус: **план, код не менялся.** Чисто UI-задача на клиенте (CSS + пропсы +Mantine). Бэкенда, схемы данных и логики не касается. + +## Суть + +Сейчас панель комментариев (правый aside, вкладка «Comments») выглядит +разреженной: крупные отступы между карточками и внутри них, большой межстрочный +интервал, тело комментария набрано базовым размером редактора (16px). На узкой +колонке это «съедает» вертикаль — на экран помещается мало комментариев, много +пустого места. + +Хотим: **уплотнить раскладку** — уменьшить внутренние/внешние отступы карточек, +зазор «аватар ↔ текст», вертикальный ритм редактора — **и уменьшить шрифт** +тела комментария, имени автора и цитаты выделения. Цель — больше комментариев +на экран без потери читабельности. + +## Где сейчас живёт «воздух» (точные места) + +Вся вёрстка панели — в фиче `apps/client/src/features/comment/`. + +### 1. Карточка комментария — [comment-list-with-tabs.tsx](apps/client/src/features/comment/components/comment-list-with-tabs.tsx) +- `renderComments`, обёртка каждого треда (~строки 121-129): + `` — `p="sm"` (12px + внутренний отступ) и `mb="sm"` (12px зазор между комментариями). +- Разделитель перед редактором ответа (~строка 148): ``. +- Вкладки (`Tabs.Panel pt="xs"`, строки 226 и 245) и пустое состояние + (`
`, строки 228 и 247) — второстепенные источники воздуха. +- Нижнее поле ввода `PageCommentInput` (строки ~361-405): `paddingTop` = `sm`, + `paddingBottom: 25`, аватар `marginTop: 10`, кнопка отправки спозиционирована + `bottom: 30`. Эти величины связаны (плавающая кнопка над полем) — трогать + осторожно. + +### 2. Элемент комментария — [comment-list-item.tsx](apps/client/src/features/comment/components/comment-list-item.tsx) +- Внешняя обёртка (строка 119): `` — 10px снизу у каждого + элемента (включая вложенные ответы). +- Шапка «аватар ↔ контент» (строка 120): `` **без** `gap` → дефолтный + `gap="md"` (16px) между аватаром и блоком с именем/телом. Это главный + горизонтальный «воздух». +- Имя автора (строка 129): `` — 14px. +- Время (строки 157-161): уже `` (12px) — оставить. +- Цитата выделения (строка 180): `{comment?.selection}` — + 14px, внутри блока `.textSelection`. + +### 3. Стили — [comment.module.css](apps/client/src/features/comment/components/comment.module.css) +- `.textSelection` (строки 9-21): `margin-top: 4px`, `padding: 8px`. +- `.commentEditor .ProseMirror :global(.ProseMirror)` (строки 35-44): + `margin-top: 10px`, `margin-bottom: 2px`, паддинги 6px. **font-size не задан** — + тело комментария наследует глобальный + `.ProseMirror { font-size: var(--mantine-font-size-md) }` (16px) из + [core.css:10](apps/client/src/features/editor/styles/core.css#L10). +- `.wrapper` (строки 1-3) — `padding: md`, **в коде не используется** (можно + игнорировать или удалить заодно). + +### 4. Внешняя рамка панели (ВНИМАНИЕ: общая) — [aside.tsx](apps/client/src/components/layouts/global/aside.tsx) +- `` (строка 47) и шапка `` с + `` (строки 50-51) дают 16px отступа по краям панели + и под заголовком. **Этот контейнер общий для трёх вкладок** aside + (`comments` / `toc` / `details`). Менять его — значит уплотнить заодно + «Содержание» и «Детали». См. «Открытые вопросы». + +Шкалы Mantine в проекте без переопределений (`theme.ts` палитру/контраст меняет, +но не размеры): шрифт `xs=12px / sm=14px / md=16px`; spacing `xs=10 / sm=12 / +md=16`. + +## Решение (точечное, в границах фичи comment) + +Базовый объём — **только компоненты `features/comment/`**, чтобы вкладки +«Содержание»/«Детали» (общий `aside.tsx`) не задеть. Уплотнение рамки панели — +отдельный опциональный пункт (см. ниже). + +### Правки по файлам + +**`comment-list-with-tabs.tsx`** +- `<Paper>` в `renderComments`: `p="sm"` → `p="xs"`, `mb="sm"` → `mb="xs"` + (12 → 10px). `shadow="sm"`, `radius="md"`, `withBorder` — оставить. +- `<Divider my={4} />` → `my={2}`. + +**`comment-list-item.tsx`** +- `<Box ref={ref} pb="xs">` → `pb={6}`. +- Шапка `<Group>` (аватар + контент, строка 120): добавить `gap="xs"` + (дефолтные 16px → 10px). НЕ трогать внутренние `<Group justify="space-between">` + и `<Group gap="xs">`, у них зазор уже задан. +- Имя: `<Text size="sm" ...>` → `size="xs"`. `fw={500}` и `lineClamp={1}` — + оставить (см. «иерархия шрифта» ниже). +- Цитата: `<Text size="sm">{comment?.selection}</Text>` → `size="xs"`. + +**`comment.module.css`** +- В `.ProseMirror :global(.ProseMirror)` добавить + `font-size: var(--mantine-font-size-sm);` (16 → 14px) и `line-height: 1.4;`, + заменить `margin-top: 10px` → `margin-top: 4px`. Остальные декларации + (`border-radius`, `max-width`, `white-space`, `word-break`, паддинги, + `margin-bottom`) — без изменений. +- `.textSelection`: `margin-top: 4px` → `2px`, `padding: 8px` → `6px`. + +### Эскиз (ключевой фрагмент CSS) + +```css +.commentEditor { + /* ... */ + .ProseMirror :global(.ProseMirror) { + border-radius: var(--mantine-radius-sm); + max-width: 100%; + white-space: pre-wrap; + word-break: break-word; + padding-left: 6px; + padding-right: 6px; + /* Denser comments: shrink body text from the global 16px ProseMirror size + to 14px and tighten the rhythm vs. the comment header. */ + font-size: var(--mantine-font-size-sm); + line-height: 1.4; + margin-top: 4px; /* was 10px */ + margin-bottom: 2px; + } +} + +.textSelection { + margin-top: 2px; /* was 4px */ + padding: 6px; /* was 8px */ + /* ...остальное без изменений... */ +} +``` + +## Тонкие моменты / edge cases + +- **Не трогать `aside.tsx` в базовом объёме.** Его `p="md"` и шапка общие для + вкладок `toc`/`details` — правка уплотнит и их. Если это нежелательно, держать + изменения строго внутри `features/comment/`. +- **Иерархия шрифта (принято).** После правок: имя — `xs` (12px, `fw=500`), + тело — `sm` (14px), время — `xs` (12px). Тело крупнее имени — это норма + (имя/мета как «капс-лейбл», тело как основной текст). +- **`font-size` ставится на внутренний `:global(.ProseMirror)`,** т.к. размер + приходит из глобального правила `core.css`. Класс-модуль `.commentEditor` + скоупит переопределение только на редактор комментариев — основной редактор + страницы не затрагивается. +- **Тёмная тема.** Меняем только размеры/отступы, цвета берутся из токенов + Mantine — отдельной проверки палитры не требуется, но визуально глянуть стоит. +- **Вложенные ответы** рендерятся тем же `CommentListItem` → уплотнение `pb`, + `gap`, шрифтов применится и к ним автоматически (так и нужно). +- **Markdown/код в теле.** `pre`/`code`/списки внутри комментария наследуют + `font-size` от `.ProseMirror` контейнера — после `font-size: sm` они тоже + станут компактнее; проверить, что код-блоки не разъезжаются. +- **Цитата выделения кликабельна** (`role="button"`, переход к месту в тексте) — + уменьшение `padding`/`size` не должно сломать зону клика; визуально проверить. +- **Нижнее поле ввода** (`PageCommentInput`) с плавающей кнопкой: величины + `paddingBottom: 25` / `bottom: 30` связаны. В базовом объёме не трогаем; если + захотим уплотнить и его — менять обе согласованно и проверить, что кнопка + отправки не наезжает на текст. + +## Тесты / проверка + +- Прогнать `pnpm --filter client lint` и `pnpm --filter client test` + (изменения косметические — падений быть не должно). +- Ручная проверка во вкладке Comments: тред с длинным телом, тред с цитатой + выделения, вложенные ответы, режим редактирования, светлая/тёмная тема, узкая + ширина aside. Убедиться, что вкладки «Содержание»/«Детали» не изменились + (если `aside.tsx` не трогали). + +## Опционально / расширения (вне базового объёма) + +- **Уплотнить рамку панели** — `aside.tsx`: `p="md"` → `p="sm"`, шапка + `mb="md"` → `mb="sm"`. Даст ощутимо меньше воздуха по краям, **но затронет все + вкладки aside** (см. «Открытые вопросы»). +- **Компактные вкладки Tabs** — `Tabs.Panel pt="xs"` → `pt={6}`, бейджи счётчиков + уже `size="sm"`. +- **Удалить мёртвый `.wrapper`** из `comment.module.css` (не используется). +- **Уменьшить аватары** с `size="sm"` до `size="xs"` в `CommentListItem` и + `PageCommentInput` для ещё большей плотности (проверить, что инициалы/картинка + не мельчат до нечитаемости). + +## Принятые решения + +Решения зафиксированы — реализовать можно сразу, без доп. согласований: + +- **Границы правки:** строго `features/comment/`. Общую рамку aside (`p="md"`, + шапка `mb="md"`) **не трогаем** — она общая с вкладками «Содержание»/«Детали», + и правка задела бы их (см. «Опционально», если позже захотим уплотнить и их). +- **Шрифт тела:** `sm` (14px) — не мельче. +- **Иерархия:** имя `xs` (12px, `fw=500`), тело `sm` (14px), время `xs` (12px). +- **Нижнее поле ввода и размер аватаров:** оставляем как есть.