Five things a wall panel showed that a wall panel should not. **A tile's body no longer clips.** It scrolled, and a box that scrolls also cuts whatever crosses its edge — which took the glow off a lit button at exactly the width where the button filled its tile, and off a gauge's arc at exactly the height where the dial filled its own. Only what is written or listed asks for a scroller now; everything else is a picture drawn to fit, and what overflows is left to the frame, which clips at the tile's edge where a shadow has already faded out. The slider's phantom scrollbar goes with it. **The selector is a selector.** Named for what it does rather than what it is, and the choice it is holding is held in the dashboard's own primary — a pill that slides between the options rather than a grey one that had to be looked for. The stored type is untouched, so no document changes meaning. **The arrangement is held off the panel's edges**, by the same distance it holds between two widgets. The ground is not held off with it: a background covers the whole panel, and only what is arranged on it has a margin. No stored panel loses a row to it. **The rail is drawn on the panel.** It was chrome bolted to the edge of the screen beside the canvas — in the app's own design rather than the dashboard's, and on a scaled canvas not even lined up with it. It now takes a column out of the canvas the way the margin does, scaled with it and wearing its look. Which cell each widget sits in is unchanged; only how big a cell is. Two of these were the same mistake twice: an unlayered rule stating `position` for everything wearing a class — `.gl-surface` on a rail placed by a utility, and a blanket lift over every child of a pressable on a pill placed by `layoutId`. Both now say it one element at a time.
116 lines
4.0 KiB
TypeScript
116 lines
4.0 KiB
TypeScript
import { useQuery } from "@tanstack/react-query"
|
|
import { createFileRoute, redirect } from "@tanstack/react-router"
|
|
|
|
import type { Dashboard } from "@/components/Dashboard/DashboardView"
|
|
import { PanelRail } from "@/components/Dashboard/PanelRail"
|
|
import { PanelSurface } from "@/components/Dashboard/PanelSurface"
|
|
import {
|
|
dashboardQueryOptions,
|
|
panelQueryOptions,
|
|
} from "@/components/Dashboard/queries"
|
|
import {
|
|
LookProvider,
|
|
useCanvasRoot,
|
|
} from "@/components/Dashboard/ui/core/look"
|
|
import { useFlowSocket } from "@/components/Flow/useFlowSocket"
|
|
import { isLoggedIn } from "@/hooks/useAuth"
|
|
import { useIsMobile } from "@/hooks/useMobile"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
/**
|
|
* What a paired device shows: the dashboards this panel was assigned.
|
|
*
|
|
* The same full-bleed surface as `/view/{name}`, with a rail down the left
|
|
* when there is more than one to switch between — so a hallway tablet and a
|
|
* workshop tablet can carry different sets without either dashboard knowing
|
|
* anything about the other.
|
|
*
|
|
* Which one is open lives in the URL, so a panel that reboots comes back where
|
|
* it was rather than at the first one.
|
|
*/
|
|
export const Route = createFileRoute("/panel/$id")({
|
|
component: PanelRoute,
|
|
validateSearch: (search: Record<string, unknown>): { d?: string } =>
|
|
typeof search.d === "string" ? { d: search.d } : {},
|
|
beforeLoad: async () => {
|
|
// Unpaired, or a credential the server stopped honouring. Either way this
|
|
// device needs a new code, not a login form it cannot type into.
|
|
if (!isLoggedIn()) {
|
|
throw redirect({ to: "/panel" })
|
|
}
|
|
},
|
|
head: ({ params }) => ({ meta: [{ title: `${params.id} - Fluksio` }] }),
|
|
})
|
|
|
|
function PanelRoute() {
|
|
const { id } = Route.useParams()
|
|
const { d } = Route.useSearch()
|
|
useFlowSocket()
|
|
|
|
const { data: panel } = useQuery(panelQueryOptions(id))
|
|
const dashboards = panel?.dashboards ?? []
|
|
// A name in the URL that this panel no longer carries falls back to the
|
|
// first, which is what a dashboard removed from under a running panel does.
|
|
const current = d && dashboards.includes(d) ? d : dashboards[0]
|
|
|
|
const { data: dashboard } = useQuery({
|
|
...dashboardQueryOptions(current ?? ""),
|
|
enabled: Boolean(current),
|
|
})
|
|
const stacked = useIsMobile()
|
|
const rail = dashboards.length > 1
|
|
// The screen is the dashboard here, so its appearance covers the whole of
|
|
// it — the rail and the letterbox around a scaled canvas included. This is
|
|
// the surface these settings exist for: a panel in a room has no other way
|
|
// to be told what to wear.
|
|
const root = useCanvasRoot(dashboard as Dashboard | undefined)
|
|
|
|
if (panel && dashboards.length === 0) {
|
|
return (
|
|
<main className="grid h-svh w-full place-items-center p-8">
|
|
<p className="text-sm text-muted-foreground">
|
|
No dashboards are assigned to this panel yet.
|
|
</p>
|
|
</main>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<main
|
|
data-look={root["data-look"]}
|
|
data-touch={root["data-touch"]}
|
|
data-palette={root["data-palette"]}
|
|
className={cn(
|
|
"relative h-svh w-full bg-background p-4 text-foreground",
|
|
root.className,
|
|
stacked ? "overflow-y-auto" : "overflow-hidden",
|
|
)}
|
|
style={root.style}
|
|
>
|
|
<LookProvider dashboard={dashboard as Dashboard | undefined}>
|
|
{dashboard ? (
|
|
<PanelSurface
|
|
dashboard={dashboard as Dashboard}
|
|
stacked={stacked}
|
|
// Drawn on the panel rather than beside it: a screen showing four
|
|
// dashboards shows the way between them too.
|
|
rail={
|
|
rail && current ? (
|
|
<PanelRail
|
|
dashboards={dashboards}
|
|
current={current}
|
|
linkFor={(name) => ({
|
|
to: "/panel/$id",
|
|
params: { id },
|
|
search: { d: name },
|
|
})}
|
|
/>
|
|
) : null
|
|
}
|
|
/>
|
|
) : null}
|
|
</LookProvider>
|
|
</main>
|
|
)
|
|
}
|