feat(editor): кнопки код-блока оверлеем + селектор языка по наведению #278

Merged
vvzvlad merged 2 commits from feat/275-codeblock-buttons into develop 2026-07-02 13:32:38 +03:00
Collaborator

Summary

closes #275. Панель управления код-блока (селектор языка + копирование) больше не занимает отдельную строку — обе кнопки ушли абсолютным оверлеем в правый верхний угол блока, а селектор языка скрыт до наведения (появляется при hover/focus/открытом дропдауне, плавно). Кнопка копирования видна всегда. В read-only селектор языка не рендерится вообще.

<pre> (редактируемый contentDOM) остаётся ПЕРВЫМ в DOM — регресс #146 (каретка съезжает на строку выше) не воспроизводится; панель уходит из потока через position:absolute. Скрытый селектор держит ширину (opacity:0), поэтому кнопка копирования не прыгает; pointer-events:none пропускает клик к коду. 3 файла (компонент + 2 css), комментарии про #146/order:-1 обновлены.

How verified

tsc --noEmit 0; eslint компонента 0 (из apps/client). Тест-файлов у код-блока нет.

Checklist

  • кнопки оверлеем в правом верхнем углу, не отдельной строкой
  • селектор языка скрыт до наведения (edit), в read-only отсутствует
  • копирование всегда видно; #146 не регрессит; print скрывает панель
## Summary closes #275. Панель управления код-блока (селектор языка + копирование) больше не занимает отдельную строку — обе кнопки ушли абсолютным оверлеем в правый верхний угол блока, а селектор языка скрыт до наведения (появляется при hover/focus/открытом дропдауне, плавно). Кнопка копирования видна всегда. В read-only селектор языка не рендерится вообще. `<pre>` (редактируемый contentDOM) остаётся ПЕРВЫМ в DOM — регресс #146 (каретка съезжает на строку выше) не воспроизводится; панель уходит из потока через `position:absolute`. Скрытый селектор держит ширину (`opacity:0`), поэтому кнопка копирования не прыгает; `pointer-events:none` пропускает клик к коду. 3 файла (компонент + 2 css), комментарии про #146/order:-1 обновлены. ## How verified `tsc --noEmit` 0; `eslint` компонента 0 (из apps/client). Тест-файлов у код-блока нет. ## Checklist - [x] кнопки оверлеем в правом верхнем углу, не отдельной строкой - [x] селектор языка скрыт до наведения (edit), в read-only отсутствует - [x] копирование всегда видно; #146 не регрессит; print скрывает панель
agent_coder added 1 commit 2026-07-02 01:21:01 +03:00
The code-block control panel (language selector + copy) took a full row above
the code. Move both to an absolute overlay in the top-right corner and hide the
language selector until the block is hovered/focused; the copy button stays
always visible. In read-only the language selector isn't rendered at all. The
<pre> (editable contentDOM) stays FIRST in the DOM so click hit-testing (#146)
is not regressed; the panel leaves the flow via position:absolute.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-02 01:21:01 +03:00
Collaborator

Ревью 5280392fc (closes #275) — контролы код-блока оверлеем, селектор языка по наведению. 3 файла (компонент + 2 css), фан-аут с фокусом на регресс #146.

Вердикт: PASS. Регресс #146 (каретка на строку выше) НЕ может вернуться, вёрстка согласована. Готово к мержу.

Что проверено

  • #146 не воспроизводится (главное): редактируемый <pre> (contentDOM) по-прежнему ПЕРВЫЙ в DOM, а панель ушла из потока в position:absolute (top-right). Переход с in-flow order:-1 на absolute строго БЕЗОПАСНЕЕ прежнего: out-of-flow элемент не участвует в box-модели contentDOM и не может сместить hit-testing каретки. Каретка-вверх-на-строку структурно невозможна.
  • Якорь корректен: оверлей .menuGroup (absolute; top/right:8px; z-index:1) привязан к .codeBlock (NodeViewWrapper), которому в code.css добавлен position:relative — ближайший позиционированный предок именно обёртка блока.
  • Reveal по hover/focus: .languageSelect держит ширину (тогглится только opacity) → кнопка копирования не прыгает; pointer-events:none в скрытом состоянии пропускает клик к коду; копирование видно всегда (класс только на root Select'а); dropdown Mantine портален (оверлей/z-index не обрезает); @media print панель по-прежнему скрывает.
  • Read-only: селектор языка теперь не рендерится вовсе ({editor.isEditable && <Select/>}), было — рендерился disabled. Осознанное изменение по #275 (у опубликованной страницы только копирование).

Объективные проверки (запущены в окружении ревью)

  • tsc --noEmit (apps/client) — exit 0. eslint в окружении не поднялся (конфиг клиента не резолвит @tanstack/eslint-plugin-query — проблема окружения) → базис eslint = прогон кодера + вычитка. Тестов у код-блока нет.
⛔ Кодеру НЕ делать — калибровочный лог (для оператора)
- [a11y] low на тач-устройствах без :hover селектор языка раскрывается только по focus-within, но `pointer-events:none` в скрытом виде мешает тапу его сфокусировать → клавиатурой достижим, тапом на hover-less тач — нет. Опц.: раскрывать по более широкому триггеру / держать Select tabbable без pointer-events:none.
- [ux] negligible read-only теряет индикатор языка (по дизайну #275).
- [ux] negligible всегда-видимая кнопка копирования оверлеем над правым-верхним углом кода — косметическое перекрытие, каретку не двигает.
Ревью **5280392fc** (closes #275) — контролы код-блока оверлеем, селектор языка по наведению. 3 файла (компонент + 2 css), фан-аут с фокусом на регресс #146. **Вердикт: PASS.** Регресс #146 (каретка на строку выше) НЕ может вернуться, вёрстка согласована. Готово к мержу. ### Что проверено - **#146 не воспроизводится (главное):** редактируемый `<pre>` (contentDOM) по-прежнему ПЕРВЫЙ в DOM, а панель ушла из потока в `position:absolute` (top-right). Переход с in-flow `order:-1` на `absolute` строго БЕЗОПАСНЕЕ прежнего: out-of-flow элемент не участвует в box-модели contentDOM и не может сместить hit-testing каретки. Каретка-вверх-на-строку структурно невозможна. - **Якорь корректен:** оверлей `.menuGroup` (`absolute; top/right:8px; z-index:1`) привязан к `.codeBlock` (NodeViewWrapper), которому в code.css добавлен `position:relative` — ближайший позиционированный предок именно обёртка блока. - **Reveal по hover/focus:** `.languageSelect` держит ширину (тогглится только `opacity`) → кнопка копирования не прыгает; `pointer-events:none` в скрытом состоянии пропускает клик к коду; копирование видно всегда (класс только на root Select'а); dropdown Mantine портален (оверлей/z-index не обрезает); `@media print` панель по-прежнему скрывает. - **Read-only:** селектор языка теперь не рендерится вовсе (`{editor.isEditable && <Select/>}`), было — рендерился disabled. Осознанное изменение по #275 (у опубликованной страницы только копирование). ### Объективные проверки (запущены в окружении ревью) - `tsc --noEmit` (apps/client) — **exit 0**. `eslint` в окружении не поднялся (конфиг клиента не резолвит `@tanstack/eslint-plugin-query` — проблема окружения) → базис eslint = прогон кодера + вычитка. Тестов у код-блока нет. ``` ⛔ Кодеру НЕ делать — калибровочный лог (для оператора) - [a11y] low на тач-устройствах без :hover селектор языка раскрывается только по focus-within, но `pointer-events:none` в скрытом виде мешает тапу его сфокусировать → клавиатурой достижим, тапом на hover-less тач — нет. Опц.: раскрывать по более широкому триггеру / держать Select tabbable без pointer-events:none. - [ux] negligible read-only теряет индикатор языка (по дизайну #275). - [ux] negligible всегда-видимая кнопка копирования оверлеем над правым-верхним углом кода — косметическое перекрытие, каретку не двигает. ``` <!-- state:review reviewed_head=5280392fc round=1 verdict=approved -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-02 01:37:36 +03:00
Collaborator

Полный 9-аспектный фан-аут (по запросу vvzvlad), поверх round-1 approve. Отдельный субагент на каждый аспект.

Вердикт: меняю на CHANGES. Инженерно всё подтверждено (см. ниже), и #146 структурно не воспроизводится — но полный проход нашёл два реальных, дешёвых пункта, которых round-1 не поднял. Оба low. Отвечай по id.

Что сделать

F1 [test coverage] Ветку read-only (селектор языка не рендерится) стоит закрыть тестомcode-block-view.tsx:79 ({editor.isEditable && <Select/>})
Раньше read-only показывал disabled-<Select>, теперь — не рендерит вовсе. Это render-условие, а не CSS, и оно НЕ покрыто. Готовый jsdom-харнесс уже есть — code-block/footnote-views.structure.test.tsx рендерит CodeBlockView с полным набором стабов. ~15 строк: замокать Select на детектируемый узел, отрендерить дважды (isEditable:true → селектор есть; false → селектора нет, copy остаётся). Остаточный риск невысок (в read-only редактор всё равно отвергает мутации), но условие тривиально регрессируемо, а тест дёшев.
Fix: добавить code-block-view.test.tsx по образцу существующего харнесса (не трогая его Select:()=>null мок).

F2 [simplification] Мёртвый проп justify="flex-end"code-block-view.tsx:71
После перехода .menuGroup на position:absolute (без width, только right:8px) Group шринк-рэпается по контенту у правого края — распределять justify нечего, проп ничего не делает. Убрать.

Подтверждено чистым (по 9 аспектам)

  • #146 НЕ воспроизводится (2 агента независимо): <pre> (contentDOM) остаётся ПЕРВЫМ в DOM, панель ушла в absolute (out-of-flow) — строго безопаснее прежнего order:-1; каретка-вверх структурно невозможна.
  • security (copy через secure useCopy, нет innerHTML, :global(.codeBlock) не течёт, isEditable-гейт — усиление), stability (условный рендер без краша, CopyButton не перемонтируется), documentation (все #146-комментарии переписаны, stale order:-1 не осталось), conventions (оверлей зеркалит html-embed-view паттерн, :global()/classNames идиоматичны), architecture (оверлей/anchor структурно верны) — LGTM.

Ниже порога (кодеру НЕ обязательно)

  • копка copy оверлеем над правым-верхним углом кода может перехватывать клик в том углу (low UX, присуще оверлею);
  • на тач без hover селектор языка не раскрыть тапом (pointer-events:none мешает), клавиатурой — да (присуще hover-reveal дизайну #275);
  • read-only теряет индикатор языка (по дизайну #275);
  • gap:4px в css — идиоматичнее пропом gap={4} на Group (стиль).

Объективные проверки round-1 в силе: tsc 0, vitest LogViewer n/a (у код-блока нет тестов), eslint в окружении не поднялся.

Полный 9-аспектный фан-аут (по запросу vvzvlad), поверх round-1 approve. Отдельный субагент на каждый аспект. **Вердикт: меняю на CHANGES.** Инженерно всё подтверждено (см. ниже), и #146 структурно не воспроизводится — но полный проход нашёл два реальных, дешёвых пункта, которых round-1 не поднял. Оба low. Отвечай по id. ### Что сделать **F1 [test coverage] Ветку read-only (селектор языка не рендерится) стоит закрыть тестом** — `code-block-view.tsx:79` (`{editor.isEditable && <Select/>}`) Раньше read-only показывал disabled-`<Select>`, теперь — не рендерит вовсе. Это render-условие, а не CSS, и оно НЕ покрыто. Готовый jsdom-харнесс уже есть — `code-block/footnote-views.structure.test.tsx` рендерит `CodeBlockView` с полным набором стабов. ~15 строк: замокать `Select` на детектируемый узел, отрендерить дважды (`isEditable:true` → селектор есть; `false` → селектора нет, copy остаётся). Остаточный риск невысок (в read-only редактор всё равно отвергает мутации), но условие тривиально регрессируемо, а тест дёшев. Fix: добавить `code-block-view.test.tsx` по образцу существующего харнесса (не трогая его `Select:()=>null` мок). **F2 [simplification] Мёртвый проп `justify="flex-end"`** — `code-block-view.tsx:71` После перехода `.menuGroup` на `position:absolute` (без width, только `right:8px`) Group шринк-рэпается по контенту у правого края — распределять `justify` нечего, проп ничего не делает. Убрать. ### Подтверждено чистым (по 9 аспектам) - **#146 НЕ воспроизводится** (2 агента независимо): `<pre>` (contentDOM) остаётся ПЕРВЫМ в DOM, панель ушла в `absolute` (out-of-flow) — строго безопаснее прежнего `order:-1`; каретка-вверх структурно невозможна. - security (copy через secure `useCopy`, нет innerHTML, `:global(.codeBlock)` не течёт, `isEditable`-гейт — усиление), stability (условный рендер без краша, CopyButton не перемонтируется), documentation (все #146-комментарии переписаны, stale `order:-1` не осталось), conventions (оверлей зеркалит `html-embed-view` паттерн, `:global()`/`classNames` идиоматичны), architecture (оверлей/anchor структурно верны) — LGTM. ### Ниже порога (кодеру НЕ обязательно) - копка copy оверлеем над правым-верхним углом кода может перехватывать клик в том углу (low UX, присуще оверлею); - на тач без hover селектор языка не раскрыть тапом (`pointer-events:none` мешает), клавиатурой — да (присуще hover-reveal дизайну #275); - read-only теряет индикатор языка (по дизайну #275); - `gap:4px` в css — идиоматичнее пропом `gap={4}` на Group (стиль). Объективные проверки round-1 в силе: `tsc` 0, `vitest` LogViewer n/a (у код-блока нет тестов), eslint в окружении не поднялся. <!-- state:review reviewed_head=5280392fc round=full verdict=changes -->
agent_reviewer added review/changes-requested and removed review/approved labels 2026-07-02 01:48:45 +03:00
Author
Collaborator

F1: fixed — добавил code-block-view.test.tsx по образцу существующего харнесса (он реально в footnote/footnote-views.structure.test.tsx, не в code-block/ — путь в ревью был чуть неточен). Тот же набор стабов; Select замокан на data-testid="language-select", ActionIconcopy-button. Два кейса: isEditable:true → селектор есть + copy есть; isEditable:false → селектора нет, copy остаётся. Существующий харнесс не трогал.
F2: fixed — убрал мёртвый justify="flex-end" у <Group> (после position:absolute без ширины Group шринк-рэпается, distribute нечего).
Проверки (из apps/client): tsc 0; eslint code-block 0; vitest code-block — 2 passed. Возвращаю review/needs.

F1: fixed — добавил `code-block-view.test.tsx` по образцу существующего харнесса (он реально в `footnote/footnote-views.structure.test.tsx`, не в `code-block/` — путь в ревью был чуть неточен). Тот же набор стабов; `Select` замокан на `data-testid="language-select"`, `ActionIcon` — `copy-button`. Два кейса: `isEditable:true` → селектор есть + copy есть; `isEditable:false` → селектора нет, copy остаётся. Существующий харнесс не трогал. F2: fixed — убрал мёртвый `justify="flex-end"` у `<Group>` (после `position:absolute` без ширины Group шринк-рэпается, distribute нечего). Проверки (из apps/client): `tsc` 0; `eslint` code-block 0; `vitest` code-block — 2 passed. Возвращаю review/needs.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-02 02:02:29 +03:00
agent_coder added 1 commit 2026-07-02 02:02:29 +03:00
F1: add code-block-view.test.tsx (mirrors the footnote structure harness) asserting
the language selector renders only when editor.isEditable, and the copy button is
present in both modes.
F2: remove the now-dead justify=flex-end on the absolutely-positioned menu Group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator

Ре-ревью ba87f4ee2 — раунд закрытия F1+F2. Полный 9-аспектный веер (отдельный субагент на каждый) на дельте.

Вердикт: PASS. Обе находки закрыты. Готово к мержу.

Проверка

  • F1 [test coverage] — ЗАКРЫТ, non-vacuous. Новый code-block-view.test.tsx рендерит РЕАЛЬНЫЙ CodeBlockView (замоканы только зависимости — tiptap/mantine/copy-button/icons/mermaid, по образцу существующего footnote-views.structure.test.tsx; Select→детектируемый testid): (editable) language-select есть + copy есть; (read-only) language-select НЕТ + copy есть. Убери гард {editor.isEditable && <Select/>} → read-only-ассерт падает. Ветка isEditable реально решается в самом компоненте.
  • F2 [simplification] — ЗАКРЫТ. justify="flex-end" снят с <Group>; изменение визуально-нейтрально (Group position:absolute только с right:8px → шринк-рэп по контенту, распределять justify нечего). Больше в code-block-view.tsx ничего не тронуто.
  • #146 по-прежнему не воспроизводится (<pre> первый в DOM, панель absolute — не задето). security/stability/regressions/conventions/documentation/architecture/coherence — LGTM.

Объективная проверка: vitest run code-block-view.test.tsx2 passed (в окружении ревью).

Ниже порога (опц.): тач-раскрытие селектора и copy-оверлей над углом кода остаются как отмечено ранее (присуще дизайну #275).

Маркер reviewed_headba87f4ee2.

Ре-ревью **ba87f4ee2** — раунд закрытия F1+F2. Полный 9-аспектный веер (отдельный субагент на каждый) на дельте. **Вердикт: PASS.** Обе находки закрыты. Готово к мержу. ### Проверка - **F1 [test coverage] — ЗАКРЫТ, non-vacuous.** Новый `code-block-view.test.tsx` рендерит РЕАЛЬНЫЙ `CodeBlockView` (замоканы только зависимости — tiptap/mantine/copy-button/icons/mermaid, по образцу существующего `footnote-views.structure.test.tsx`; `Select`→детектируемый testid): (editable) `language-select` есть + copy есть; (read-only) `language-select` НЕТ + copy есть. Убери гард `{editor.isEditable && <Select/>}` → read-only-ассерт падает. Ветка `isEditable` реально решается в самом компоненте. - **F2 [simplification] — ЗАКРЫТ.** `justify="flex-end"` снят с `<Group>`; изменение визуально-нейтрально (Group `position:absolute` только с `right:8px` → шринк-рэп по контенту, распределять `justify` нечего). Больше в code-block-view.tsx ничего не тронуто. - **#146 по-прежнему не воспроизводится** (`<pre>` первый в DOM, панель absolute — не задето). security/stability/regressions/conventions/documentation/architecture/coherence — LGTM. Объективная проверка: `vitest run code-block-view.test.tsx` — **2 passed** (в окружении ревью). Ниже порога (опц.): тач-раскрытие селектора и copy-оверлей над углом кода остаются как отмечено ранее (присуще дизайну #275). Маркер `reviewed_head` — `ba87f4ee2`. <!-- state:review reviewed_head=ba87f4ee2 round=2 verdict=approved -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-02 02:28:47 +03:00
vvzvlad merged commit 2624825a3a into develop 2026-07-02 13:32:38 +03:00
vvzvlad deleted branch feat/275-codeblock-buttons 2026-07-02 13:32:44 +03:00
Sign in to join this conversation.