Files
app/frontend/src/routes/view.$name.tsx
T
stroblmeandClaude Opus 5 092a5333e8 Add the dashboard settings channel, wired for theme and lock
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
2026-08-22 13:17:56 +02:00

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>
)
}