Files
app/frontend/src/components/Dashboard/PanelRail.tsx
T

76 lines
2.7 KiB
TypeScript

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"
/**
* Switching between the dashboards one panel was assigned.
*
* Permanent rather than a menu behind a button: a wall panel is glanced at,
* and something you have to open first is something nobody opens. It costs
* about a fortieth of the canvas, which `CanvasSurface` absorbs by scaling —
* the grid itself never changes, so an arrangement made without the rail still
* fits with it.
*
* Drawn in the dashboard's own look, because it hangs on the same screen: a
* rail wearing the app's chrome beside a glass panel is two designs at once.
* On a phone it is a row the caller floats along the bottom.
*
* Reading the dashboards it links to is also what fills the labels, and it
* warms the cache for the neighbours so a switch draws immediately.
*/
export function PanelRail({
dashboards,
current,
linkFor,
horizontal,
}: {
dashboards: string[]
current: string
linkFor: (name: string) => LinkProps
horizontal?: boolean
}) {
const { Rail } = useUi()
const box = useRef<HTMLDivElement>(null)
const still = useReducedMotion()
const entries = useQueries({
queries: dashboards.map((name) => dashboardQueryOptions(name)),
combine: (results) =>
results.map((result, index) => ({
name: dashboards[index],
label: result.data?.title || dashboards[index],
icon: result.data?.icon ?? "",
active: dashboards[index] === current,
link: linkFor(dashboards[index]),
})),
})
// 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 (
<div ref={box} className="contents">
<Rail entries={entries} horizontal={horizontal} />
</div>
)
}