A dashboard could only ever receive as a set of tiles. This adds the dashboard itself as a receiver: `settings` maps a name to a value plus an optional binding. Unbound, the setting is simply its value — a wall panel that is always dark costs no flow. Bound, a flow drives it live and the value is the fallback. Two settings are wired: `theme` (system/light/dark) and `locked` (read-only). There is no schedule field on purpose — a node publishing to the bound message on a cron is what a schedule is here, which is the point of a channel. - `messages_for()` now walks a dashboard's bound settings as well as its widgets' bindings. Without this a paired screen is refused its own theme message, on the one surface the setting exists for; it bounds the socket too. - `locked` is gated in `usePublish`, so every control inherits it, and each control also draws itself disabled — a dead button reads as broken otherwise. The panel surface says Read-only in the corner. - The theme is a class on the dashboard's own surface, never the root: inside the app shell it must not flip the chrome. `.light` gains the tokens `.dark` already had (mirrored in the index repo) so both directions work on a subtree. - Settings bindings are type-checked from the document alone, the rule widget bindings follow, and mirrored on the server. - A bound setting is drawn on the flow canvas as a dashboard-level endpoint. - The demo's house flow now publishes `home.panel_theme`, which the demo dashboard's theme binds to: the panel goes dark after sunset, at no tile cost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tULRZJUkZsw7rMJ3h4xvu
57 lines
2.1 KiB
TypeScript
57 lines
2.1 KiB
TypeScript
import { useQuery } from "@tanstack/react-query"
|
|
import { createFileRoute, redirect } from "@tanstack/react-router"
|
|
|
|
import type { Dashboard } from "@/components/Dashboard/DashboardView"
|
|
import { PanelSurface } from "@/components/Dashboard/PanelSurface"
|
|
import { dashboardQueryOptions } from "@/components/Dashboard/queries"
|
|
import { useDashboardTheme } from "@/components/Dashboard/settings"
|
|
import { useFlowSocket } from "@/components/Flow/useFlowSocket"
|
|
import { isLoggedIn } from "@/hooks/useAuth"
|
|
import { useIsMobile } from "@/hooks/useMobile"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
/**
|
|
* What a wall panel is pointed at when it shows one dashboard and nothing else.
|
|
*
|
|
* Deliberately outside both shells: no sidebar, no footer, no editing, and no
|
|
* column cap — the widgets are the whole page. Route splitting means a panel
|
|
* never downloads the editor or the grid library either.
|
|
*
|
|
* A device that was paired to a panel uses `/panel/{id}` instead, which is the
|
|
* same surface with a rail for switching between the dashboards it was
|
|
* assigned. This route stays for a browser tab pointed straight at one.
|
|
*/
|
|
export const Route = createFileRoute("/view/$name")({
|
|
component: PanelView,
|
|
beforeLoad: async () => {
|
|
if (!isLoggedIn()) {
|
|
throw redirect({ to: "/login" })
|
|
}
|
|
},
|
|
head: ({ params }) => ({ meta: [{ title: `${params.name} - Fluksio` }] }),
|
|
})
|
|
|
|
function PanelView() {
|
|
const { name } = Route.useParams()
|
|
useFlowSocket()
|
|
const { data: dashboard } = useQuery(dashboardQueryOptions(name))
|
|
const stacked = useIsMobile()
|
|
// A tab pointed at one dashboard is that dashboard, so its theme covers the
|
|
// whole page rather than only the canvas inside it.
|
|
const theme = useDashboardTheme(dashboard as Dashboard | undefined)
|
|
|
|
if (!dashboard) return <main className="h-svh w-full" />
|
|
|
|
return (
|
|
<main
|
|
className={cn(
|
|
"h-svh w-full bg-background p-4 text-foreground",
|
|
theme,
|
|
stacked ? "overflow-y-auto" : "overflow-hidden",
|
|
)}
|
|
>
|
|
<PanelSurface dashboard={dashboard as Dashboard} stacked={stacked} />
|
|
</main>
|
|
)
|
|
}
|