fix(settings): track scroll indicator at viewport midpoint

The settings sidebar highlight lagged well behind the section the user
was actually reading. The old heuristic picked the first section whose
top was near the scroll container's top, so a tall section could still
occupy most of the viewport while the sidebar had already advanced.

Switch to a probe line at 40% down the viewport and highlight whichever
section straddles it, falling back to the last section above the probe,
with a bottom-of-scroll override so short trailing sections still get
selected when the user scrolls to the end.
This commit is contained in:
Neil 2026-04-17 15:53:09 -07:00
parent 1daf9d1b94
commit aee9f2c6c1

View file

@ -359,10 +359,39 @@ function Settings(): React.JSX.Element {
return
}
const containerTop = container.getBoundingClientRect().top
const candidate =
sections.find((section) => section.getBoundingClientRect().top - containerTop >= -24) ??
sections.at(-1)
// Why: highlight the section that the user is actually reading.
// We pick the section whose body crosses a probe line ~40% down the
// viewport (roughly the middle, biased slightly up toward where the
// eye naturally focuses). Earlier logic used the first section with
// its top near the container top, which lagged badly — a section
// could still fill most of the viewport while the sidebar had already
// advanced to the next one.
const containerRect = container.getBoundingClientRect()
const probeY = containerRect.top + containerRect.height * 0.4
// If we've scrolled to the very bottom, force-highlight the last
// section even when it's too short to reach the probe line.
const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 2
let candidate: HTMLElement | undefined
if (atBottom) {
candidate = sections.at(-1)
} else {
for (const section of sections) {
const rect = section.getBoundingClientRect()
if (rect.top <= probeY && rect.bottom > probeY) {
candidate = section
break
}
if (rect.top <= probeY) {
// Last section whose heading is above the probe line — used
// when no section straddles the probe (e.g. between sections,
// or when the probe sits in the gutter above the first one).
candidate = section
}
}
candidate ??= sections.at(0)
}
if (!candidate) {
return
}