Panels: per-device dashboard sets, paired by code
A panel is one screen and the ordered set of whole dashboards it shows, so a hallway tablet and a workshop tablet carry different sets without either dashboard knowing about the other. More than one and the device draws a rail to switch between them — the same rail the editor puts on screen, because the wall has it and it takes room off the canvas. A screen has no keyboard, so it pairs rather than logs in: it shows a six-character code, somebody approves it against a panel from the dashboards overview, and the credential that mints reaches that panel's published dashboards and the message endpoints its widgets speak, and nothing else. Deleting the panel revokes it. Closes the per-device view and the kiosk credential; supersedes the multi-page/multi-section UI, since a page is now a dashboard of its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AHpLJHozysQXjsxAyU1WHj
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { useQueries } from "@tanstack/react-query"
|
||||
import { Link, type LinkProps } from "@tanstack/react-router"
|
||||
|
||||
import { dashboardQueryOptions } from "@/components/Dashboard/queries"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
/** How much room the rail takes, canvas insets included. Mirrors the side
|
||||
* panel's `27rem`: the button plus the gutters either side of it. */
|
||||
export const RAIL_INSET = "4.5rem"
|
||||
|
||||
/** Two letters off the title, so a rail of four reads as four different things. */
|
||||
function initials(label: string): string {
|
||||
const words = label.split(/[\s_-]+/).filter(Boolean)
|
||||
if (words.length === 0) return "?"
|
||||
if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
|
||||
return (words[0][0] + words[1][0]).toUpperCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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,
|
||||
className,
|
||||
}: {
|
||||
dashboards: string[]
|
||||
current: string
|
||||
linkFor: (name: string) => LinkProps
|
||||
className?: string
|
||||
}) {
|
||||
const titles = useQueries({
|
||||
queries: dashboards.map((name) => dashboardQueryOptions(name)),
|
||||
combine: (results) =>
|
||||
results.map((result, index) => result.data?.title || dashboards[index]),
|
||||
})
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="Dashboards on this panel"
|
||||
data-testid="panel-rail"
|
||||
className={cn(
|
||||
"pointer-events-auto absolute inset-y-4 left-4 z-10 flex w-12 flex-col items-center gap-1 overflow-y-auto rounded-lg border border-border bg-card/80 p-1 shadow-e2 backdrop-blur-md",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{dashboards.map((name, index) => {
|
||||
const label = titles[index]
|
||||
const active = name === current
|
||||
return (
|
||||
<Tooltip key={name}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
// `icon-lg` at every width on purpose: a rail is a touch
|
||||
// surface whatever the screen's size, and the panel it hangs
|
||||
// on is 1024 wide as often as it is 400.
|
||||
size="icon-lg"
|
||||
className={cn(
|
||||
"shrink-0 text-xs font-medium text-muted-foreground",
|
||||
active && "bg-accent text-foreground",
|
||||
)}
|
||||
aria-current={active ? "page" : undefined}
|
||||
data-testid={`panel-rail-${name}`}
|
||||
>
|
||||
<Link {...linkFor(name)} aria-label={label}>
|
||||
{initials(label)}
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user