fix(ui): F8-09 owner-feedback — fixed TOC trigger + heading offset (#52)
Tests · UI / test (push) Has been cancelled
Tests · UI / test (pull_request) Successful in 2m44s

Owner-reported regressions in Firefox + Safari on desktop after the
initial F8-09 patch landed:

1. The TOC trigger rode up with the page during scroll instead of
   staying pinned to the viewport (mobile worked, desktop did not).
2. Clicking a popover item scrolled the matching section so its
   heading went up under the chrome — only the table body was visible.

Root cause for (1): the in-game shell declares `overflow-y: auto`
on `.active-view-host` so mobile (where `.game-shell` is fixed at
`inset: 0`) has an internal scroll region. On desktop the host
grows with content, no overflow ever engages, and the document
body becomes the actual scroll container. Per CSS spec the host
remains the "scrollport" for any `position: sticky` descendant, so
the trigger inside the report column never sees the scroll event
and rides up with the body content.

Fix:

- Swap the trigger from `position: sticky` to `position: fixed`.
  The component is mounted only while the report active view is on
  screen, so the fixed element is naturally tied to the view's
  lifetime. Anchor at `top: 4rem` (below the in-game header), and
  on `min-width: 1024px` shift `right` by 18 rem to clear the
  always-on sidebar; below 1024 px the sidebar is an overlay so
  the default `right: 1.25rem` matches the report's right padding.
- Add `padding-top: 4.5rem` to `.report-view` (4rem mobile) so the
  first section heading does not land under the trigger at scroll
  position 0.
- Add `scroll-margin-top: 7.5rem` to every `<section
  id="report-…">` so `scrollIntoView({ block: "start" })` lands
  the heading below the trigger after a popover-driven jump.
- Sync `ui/docs/report-view.md` §"Table of contents and active
  highlight" with the new positioning rationale.

Tests: `pnpm check`, `pnpm test` (821), `pnpm test:e2e
report-sections` (4 projects) all green.

Refs: #52 (#43 umbrella).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-27 19:45:44 +02:00
parent cfbe052242
commit 1b2c13ecd6
3 changed files with 78 additions and 12 deletions
+25 -4
View File
@@ -85,8 +85,8 @@ the body — both surfaces stay in sync by construction.
## Table of contents and active highlight
`report/report-toc.svelte` renders a single sticky icon-popup
trigger in the top-right corner of the report column. The trigger
`report/report-toc.svelte` renders a single icon-popup trigger
pinned to the top-right corner of the report column. The trigger
shows `≡` followed by the title of the currently-active section
(CSS-clamped with `text-overflow: ellipsis` so a long RU title
cannot bloat the button). Clicking opens an anchored popover
@@ -97,8 +97,29 @@ the matching `<section id="report-<slug>">` into view via
`scrollIntoView` (with `prefers-reduced-motion` falling back to
`behavior: "auto"`).
On viewports below `768.98 px` the same surface re-styles into a
fixed bottom-sheet anchored above the layout-owned bottom-tabs
The trigger uses `position: fixed` instead of `position: sticky`.
Per the CSS sticky spec a sticky element sticks within its nearest
ancestor with non-`visible` overflow — and `.active-view-host`
declares `overflow-y: auto` for the mobile scroll story. On
desktop the host grows with content and the document body becomes
the actual scroll container, so a sticky trigger inside the report
column never receives a scroll event and rides up with the page
content. A fixed trigger sidesteps the chain entirely; the
component is mounted only while the report active view is on
screen, so the fixed element is naturally tied to the view's
lifetime. The desktop offset is `right: calc(18 rem + 1.25 rem)`
to clear the always-on `lib/sidebar/sidebar.svelte`; below
1024 px the sidebar collapses to an overlay drawer, so the
default `right: 1.25 rem` matches the report's right padding. The
report-view itself adds a top padding equal to the trigger's
viewport offset plus its height so the first section's heading
does not render under the trigger at scroll position 0, and every
`<section id="report-…">` gets `scroll-margin-top: 7.5rem` so
`scrollIntoView({ block: "start" })` lands the heading below the
trigger after a popover-driven jump.
On viewports below `768.98 px` the popover surface re-styles into
a fixed bottom-sheet anchored above the layout-owned bottom-tabs
bar (mirrors `lib/active-view/map-toggles.svelte`), so the same
trigger and the same menuitem list serve desktop and mobile.