From 4d63f588062507dd7271aec6444319dc189b7ee4 Mon Sep 17 00:00:00 2001 From: stroblme Date: Tue, 6 Oct 2026 07:24:53 +0200 Subject: [PATCH] Scroll the current dashboard to the middle of the phone rail On mount and on every switch the horizontal rail scrolls itself (not the page) so the active entry is centred, instantly under reduced motion. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013GAeg4Jo6xjZzjwy9QDWMQ --- .../src/components/Dashboard/PanelRail.tsx | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Dashboard/PanelRail.tsx b/frontend/src/components/Dashboard/PanelRail.tsx index d179edf..a48126b 100644 --- a/frontend/src/components/Dashboard/PanelRail.tsx +++ b/frontend/src/components/Dashboard/PanelRail.tsx @@ -1,5 +1,7 @@ import { useQueries } from "@tanstack/react-query" import type { LinkProps } from "@tanstack/react-router" +import { useReducedMotion } from "motion/react" +import { useEffect, useRef } from "react" import { dashboardQueryOptions } from "@/components/Dashboard/queries" import { useUi } from "./ui" @@ -32,6 +34,8 @@ export function PanelRail({ horizontal?: boolean }) { const { Rail } = useUi() + const box = useRef(null) + const still = useReducedMotion() const entries = useQueries({ queries: dashboards.map((name) => dashboardQueryOptions(name)), combine: (results) => @@ -44,5 +48,28 @@ export function PanelRail({ })), }) - return + // A row of more dashboards than fit scrolls sideways, so the current one is + // brought to its middle. Only the row moves: `scrollIntoView` would scroll + // the page under it too. + // biome-ignore lint/correctness/useExhaustiveDependencies: a new current dashboard is what scrolls. + useEffect(() => { + if (!horizontal) return + const item = box.current?.querySelector('[aria-current="page"]') + const row = item?.closest(".dui-rail") + if (!item || !row) return + const at = item.getBoundingClientRect() + const frame = row.getBoundingClientRect() + row.scrollBy({ + left: at.left + at.width / 2 - (frame.left + frame.width / 2), + behavior: still ? "instant" : "smooth", + }) + }, [current, horizontal, still]) + + // `contents`, so the wrapper is only something to search from: the caller + // places the rail itself. + return ( +
+ +
+ ) }