feat(ui): F8-09 — turn report sticky icon-popup section menu #67

Merged
developer merged 2 commits from feature/issue-52-report-toc-popup into development 2026-05-27 17:52:59 +00:00
3 changed files with 78 additions and 12 deletions
Showing only changes of commit 1b2c13ecd6 - Show all commits
+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.
+17 -4
View File
@@ -134,10 +134,14 @@ TOC and the body iterate the same data.
</div>
<style>
/*
`padding-top` clears the fixed TOC trigger that lives in the
viewport's top-right corner (`report/report-toc.svelte`,
`position: fixed; top: 4rem`). Without this padding the first
section (galaxy-summary) renders directly under the trigger.
*/
.report-view {
display: flex;
flex-direction: column;
padding: 1rem 1.25rem 2rem;
padding: 4.5rem 1.25rem 2rem;
font-family: system-ui, sans-serif;
}
.report-body {
@@ -146,9 +150,18 @@ TOC and the body iterate the same data.
flex-direction: column;
gap: 1.75rem;
}
/*
`scroll-margin-top` lets `scrollIntoView({ block: "start" })`
land the section *heading* in view (not behind the fixed
trigger or the sticky in-game header). Budget: ~3 rem header +
~3 rem trigger area + ~1.5 rem breathing room.
*/
.report-body :global(section[id^="report-"]) {
scroll-margin-top: 7.5rem;
}
@media (max-width: 767.98px) {
.report-view {
padding: 0.75rem;
padding: 4rem 0.75rem 0.75rem;
}
}
</style>
@@ -143,14 +143,43 @@ in F8-09: that switch is available in the app-shell view menu.
</div>
<style>
/*
The trigger uses `position: fixed` rather than `position: sticky`
because on desktop the in-game shell's `.active-view-host`
declares `overflow-y: auto` while staying tall enough that no
overflow actually engages — the document body scrolls instead.
Per the CSS sticky spec the host is the "scrollport" for any
sticky descendant, so a sticky trigger inside the report column
never receives a scroll event and rides up with the page. Fixed
positioning anchors the trigger to the viewport directly; the
component is only mounted while the report active view is on
screen, so the fixed element is naturally tied to the view's
lifetime.
`top: 4rem` clears the sticky header (~3 rem). On ≥ 1024 px the
sidebar (`lib/sidebar/sidebar.svelte`) is always visible and
occupies the right 18 rem of the viewport, so the trigger has to
be pushed left by that much to stay inside the report column.
Below 1024 px the sidebar collapses into an overlay drawer,
so a viewport-right anchor of 1.25 rem matches the report's
right padding.
When the history banner is showing (historical turn view,
~2 rem extra) the trigger sits at the banner's lower edge —
acceptable for the rare historical-turn read path.
*/
.report-toc {
position: sticky;
top: 0.5rem;
align-self: flex-end;
margin-left: auto;
position: fixed;
top: 4rem;
right: 1.25rem;
z-index: 30;
font-family: system-ui, sans-serif;
}
@media (min-width: 1024px) {
.report-toc {
right: calc(18rem + 1.25rem);
}
}
.trigger {
display: inline-flex;
align-items: center;
@@ -221,6 +250,9 @@ in F8-09: that switch is available in the app-shell view menu.
border-left-color: var(--color-accent);
}
@media (max-width: 767.98px) {
.report-toc {
right: 0.75rem;
}
.surface {
position: fixed;
top: auto;