feat(client): сноски — рендер без сдвига (номер инлайн через ::before) и шрифтом sm #421

Merged
vvzvlad merged 1 commits from feat/420-footnote-render into develop 2026-07-10 04:26:51 +03:00
Collaborator

Summary

Блок сносок внизу страницы рендерился с висячим отступом (номер N. жил в отдельном узком столбце .definitionMarker через order:-1; min-width:1.5em, текст и переносы уезжали вправо) и кеглем как у тела (16px).

Реализован рекомендованный вариант: столбец-маркер удалён, номер рисуется инлайн в начале первого параграфа контента через .definitionContent > :first-child::before из CSS-переменной --footnote-number (она по-прежнему проставляется на обёртке .definition). Кегль сносок снижен до var(--mantine-font-size-sm) (14px, как .listHeading).

closes #420

How verified

  • cd apps/client && pnpm exec vitest run src/features/editor/components/footnote/7 passed (7), footnote-definition-view.tsx 100% покрытие. Ненулевой код возврата — только глобальный порог branch-coverage при прогоне подмножества, не падение теста.
  • Инвариант #146 (contentDOM — первый DOM-ребёнок): NodeViewContent теперь буквально первый ребёнок (chrome-маркера перед ним больше нет), ::before — псевдоэлемент, в DOM не входит, ProseMirror его не видит → каретка/hit-testing не ломаются. Структурный тест footnote-views.structure.test.tsx не менялся и остаётся невакуумным (single-ref бэклинк contentEditable=falsenonEditable.length > 0).
  • #168 (мульти-бэклинки ↩ a b c) не тронут; .backLink/.backLinkArrow в относительных em — унаследовали меньший кегль.
  • Экспорт/модель: номер целиком из CSS-var/::before, в модель документа и маркдаун не попадает — регрессий экспорта нет.

Внутренний цикл: 1 проход coder + 1 внутреннее ревью (read-only) → замечаний нет (fallback-вариант со <span> не понадобился, инвариант каретки сохранён рекомендованным путём).

Checklist

  • критерии приёмки из #420 выполнены (flush-left номер+текст, переносы к левому полю, кегль sm, бэклинки работают и выровнены, #146 не сломан)
  • вне заявленного scope ничего не менялось (только footnote.module.css + footnote-definition-view.tsx)
## Summary Блок сносок внизу страницы рендерился с висячим отступом (номер `N.` жил в отдельном узком столбце `.definitionMarker` через `order:-1; min-width:1.5em`, текст и переносы уезжали вправо) и кеглем как у тела (16px). Реализован рекомендованный вариант: столбец-маркер удалён, номер рисуется инлайн в начале первого параграфа контента через `.definitionContent > :first-child::before` из CSS-переменной `--footnote-number` (она по-прежнему проставляется на обёртке `.definition`). Кегль сносок снижен до `var(--mantine-font-size-sm)` (14px, как `.listHeading`). closes #420 ## How verified - `cd apps/client && pnpm exec vitest run src/features/editor/components/footnote/` → **7 passed (7)**, `footnote-definition-view.tsx` 100% покрытие. Ненулевой код возврата — только глобальный порог branch-coverage при прогоне подмножества, не падение теста. - Инвариант #146 (contentDOM — первый DOM-ребёнок): `NodeViewContent` теперь буквально первый ребёнок (chrome-маркера перед ним больше нет), `::before` — псевдоэлемент, в DOM не входит, ProseMirror его не видит → каретка/hit-testing не ломаются. Структурный тест `footnote-views.structure.test.tsx` не менялся и остаётся невакуумным (single-ref бэклинк `contentEditable=false` → `nonEditable.length > 0`). - #168 (мульти-бэклинки `↩ a b c`) не тронут; `.backLink`/`.backLinkArrow` в относительных `em` — унаследовали меньший кегль. - Экспорт/модель: номер целиком из CSS-var/`::before`, в модель документа и маркдаун не попадает — регрессий экспорта нет. Внутренний цикл: 1 проход coder + 1 внутреннее ревью (read-only) → замечаний нет (fallback-вариант со `<span>` не понадобился, инвариант каретки сохранён рекомендованным путём). ## Checklist - [x] критерии приёмки из #420 выполнены (flush-left номер+текст, переносы к левому полю, кегль sm, бэклинки работают и выровнены, #146 не сломан) - [x] вне заявленного scope ничего не менялось (только footnote.module.css + footnote-definition-view.tsx)
agent_coder added 1 commit 2026-07-09 23:13:08 +03:00
Убран отдельный столбец-маркер .definitionMarker (order:-1, min-width:1.5em),
дававший висячий отступ. Номер сноски теперь рисуется инлайн в начале первого
параграфа через .definitionContent > :first-child::before из CSS-переменной
--footnote-number (в модель документа не попадает, экспорт не затрагивает).
Кегль сносок уменьшен до var(--mantine-font-size-sm). Инвариант #146 (contentDOM
первый в DOM) и логика мульти-бэклинков #168 сохранены.

closes #420

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

Ревью — #421 · feat(client): сноски — рендер без сдвига (номер инлайн через ::before) и шрифтом sm

Вердикт: OK (PASS). Чистое презентационное изменение (клиент, 2 файла, +25/-30): столбец-маркер .definitionMarker удалён, номер рисуется инлайн через .definitionContent > :first-child::before из CSS-var --footnote-number; кегль сносок снижен до sm. Висячий отступ устранён, инвариант #146 сохранён (даже усилён). closes #420.

Как проверено (гейт, clean-room /home/claude/gm-ci)

  • apps/client tsc (tsconfig.json) — PASS (0 ошибок), workspace-пакеты собраны.
  • vitest src/features/editor/components/footnote/ c --coverage.enabled=false7/7 PASS, exit 0. (Ненулевой код возврата в PR-описании — только глобальный branch-threshold при прогоне подмножества, не падение теста; подтверждено.)
  • #146 (contentDOM — первый DOM-ребёнок): маркер-span (шёл после NodeViewContent в DOM, влево через order:-1) удалён → NodeViewContent теперь буквально первый ребёнок, число — псевдоэлемент ::before (в DOM/модель ProseMirror не входит) → каретка/hit-testing целы. Структурный гвард footnote-views.structure.test.tsx не менялся и остаётся невакуумным: single-ref бэклинк по-прежнему contentEditable={false}nonEditable.length > 0.
  • Байт-трасса CSS-var: style --footnote-number = "1" + content: var(...) ". "1. ; фолбэк "?" совпадает с getFootnoteNumber(...) ?? "?". Дангл-ссылок на .definitionMarker в кодовой базе нет.
  • Экспорт/копирование: номер целиком в CSS-var/::before (user-select:none), в модель/маркдаун не попадает — регрессий нет.

Отдельно проверено: a11y (снятие aria-hidden) — НЕ регрессия

Старый маркер был aria-hidden="true", ::before — нет, и его контент попадает в AX-дерево → скринридер теперь озвучивает номер. Разобрано против кода: референс-сторона .reference::after уже рендерит номер псевдоэлементом БЕЗ aria-hidden — озвучивание номеров сносок уже штатное поведение проекта. Старый aria-hidden на дефиниции стоял потому, что DOM-маркер читался НЕ ПО ПОРЯДКУ (после текста, т.к. в DOM он шёл после контента, влево — через flex order); ::before ставит номер в правильную позицию чтения (перед текстом → «1. текст»). То есть снятие aria-hidden — согласование дефиниции с уже принятым паттерном референса, а не регрессия. Escalate снят.

DROP — кодеру НЕ делать (лог калибровки)
  • [below-threshold] medium/high [regressions] «a11y: номер теперь озвучивается скринридером» — footnote-definition-view.tsx:59 — проверено НЕ регрессией: .reference::after уже озвучивает номер без aria-hidden (штатно); старый aria-hidden компенсировал out-of-order чтение DOM-маркера, ::before читается по порядку → согласование, а не регресс. Escalate снят.
  • [below-threshold] medium [documentation] JSDoc «a decorative number marker» — footnote-definition-view.tsx:21-28 — прежний текст, «маркер» визуально всё ещё есть; PR-комментарий ниже уже поясняет CSS-::before. Код верен.
  • [below-threshold] low [documentation] коммент говорит «.definitionContent ::before», селектор — .definitionContent > :first-child::beforefootnote-definition-view.tsx:55 — неточность в тексте, смысл ясен; не дефект.

9 аспектов (security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence). Security: number — всегда положительный int или "?", CSS-инъекции нет. Simplification/Architecture/Coherence: изменение усиливает #146 (меньше не-редактируемых DOM-узлов у contentDOM), скоуп-крипа нет (кегль sm = часть визуального фикса, «как .listHeading»). Escalate нет.

## Ревью — #421 · feat(client): сноски — рендер без сдвига (номер инлайн через ::before) и шрифтом sm **Вердикт: OK (PASS).** Чистое презентационное изменение (клиент, 2 файла, +25/-30): столбец-маркер `.definitionMarker` удалён, номер рисуется инлайн через `.definitionContent > :first-child::before` из CSS-var `--footnote-number`; кегль сносок снижен до `sm`. Висячий отступ устранён, инвариант #146 сохранён (даже усилён). closes #420. ### Как проверено (гейт, clean-room `/home/claude/gm-ci`) - `apps/client` tsc (`tsconfig.json`) — **PASS** (0 ошибок), workspace-пакеты собраны. - vitest `src/features/editor/components/footnote/` c `--coverage.enabled=false` — **7/7 PASS**, exit 0. (Ненулевой код возврата в PR-описании — только глобальный branch-threshold при прогоне подмножества, не падение теста; подтверждено.) - **#146 (contentDOM — первый DOM-ребёнок)**: маркер-span (шёл после `NodeViewContent` в DOM, влево через `order:-1`) удалён → `NodeViewContent` теперь буквально первый ребёнок, число — псевдоэлемент `::before` (в DOM/модель ProseMirror не входит) → каретка/hit-testing целы. Структурный гвард `footnote-views.structure.test.tsx` не менялся и **остаётся невакуумным**: single-ref бэклинк по-прежнему `contentEditable={false}` → `nonEditable.length > 0`. - Байт-трасса CSS-var: `style --footnote-number = "1"` + `content: var(...) ". "` → `1. `; фолбэк `"?"` совпадает с `getFootnoteNumber(...) ?? "?"`. Дангл-ссылок на `.definitionMarker` в кодовой базе нет. - Экспорт/копирование: номер целиком в CSS-var/`::before` (`user-select:none`), в модель/маркдаун не попадает — регрессий нет. ### Отдельно проверено: a11y (снятие `aria-hidden`) — НЕ регрессия Старый маркер был `aria-hidden="true"`, `::before` — нет, и его контент попадает в AX-дерево → скринридер теперь озвучивает номер. Разобрано против кода: **референс-сторона `.reference::after` уже рендерит номер псевдоэлементом БЕЗ `aria-hidden`** — озвучивание номеров сносок уже штатное поведение проекта. Старый `aria-hidden` на дефиниции стоял потому, что DOM-маркер читался НЕ ПО ПОРЯДКУ (после текста, т.к. в DOM он шёл после контента, влево — через flex `order`); `::before` ставит номер в правильную позицию чтения (перед текстом → «1. текст»). То есть снятие `aria-hidden` — согласование дефиниции с уже принятым паттерном референса, а не регрессия. Escalate снят. <details><summary>⛔ DROP — кодеру НЕ делать (лог калибровки)</summary> - `[below-threshold]` medium/high **[regressions]** «a11y: номер теперь озвучивается скринридером» — `footnote-definition-view.tsx:59` — проверено НЕ регрессией: `.reference::after` уже озвучивает номер без `aria-hidden` (штатно); старый `aria-hidden` компенсировал out-of-order чтение DOM-маркера, `::before` читается по порядку → согласование, а не регресс. Escalate снят. - `[below-threshold]` medium **[documentation]** JSDoc «a decorative number marker» — `footnote-definition-view.tsx:21-28` — прежний текст, «маркер» визуально всё ещё есть; PR-комментарий ниже уже поясняет CSS-`::before`. Код верен. - `[below-threshold]` low **[documentation]** коммент говорит «`.definitionContent ::before`», селектор — `.definitionContent > :first-child::before` — `footnote-definition-view.tsx:55` — неточность в тексте, смысл ясен; не дефект. </details> <sub>9 аспектов (security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence). Security: `number` — всегда положительный int или `"?"`, CSS-инъекции нет. Simplification/Architecture/Coherence: изменение усиливает #146 (меньше не-редактируемых DOM-узлов у contentDOM), скоуп-крипа нет (кегль `sm` = часть визуального фикса, «как `.listHeading`»). Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-09 23:23:07 +03:00
vvzvlad merged commit d0d2a7880f into develop 2026-07-10 04:26:51 +03:00
Sign in to join this conversation.