76 lines
2.7 KiB
TypeScript
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>
|
|
)
|
|
}
|