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
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { Lock } from "lucide-react"
|
||||
|
||||
import type { Dashboard } from "@/components/Dashboard/DashboardView"
|
||||
import {
|
||||
CanvasSurface,
|
||||
DashboardView,
|
||||
} from "@/components/Dashboard/DashboardView"
|
||||
import { useDashboardLocked } from "@/components/Dashboard/settings"
|
||||
|
||||
/**
|
||||
* One dashboard filling whatever screen it landed on.
|
||||
@@ -10,6 +13,10 @@ import {
|
||||
* The whole of what a wall panel draws, shared by the single-dashboard route
|
||||
* (`/view/{name}`) and the paired-panel one (`/panel/{id}`) so a device shows
|
||||
* the same thing either way — the second merely has a rail beside it.
|
||||
*
|
||||
* The dashboard's theme is stated by the route rather than here: on these two
|
||||
* the screen *is* the dashboard, so it has to cover the letterbox and the rail
|
||||
* as well as the canvas.
|
||||
*/
|
||||
export function PanelSurface({
|
||||
dashboard,
|
||||
@@ -20,13 +27,48 @@ export function PanelSurface({
|
||||
* its size, which reads as nothing at all. Stack it instead. */
|
||||
stacked?: boolean
|
||||
}) {
|
||||
if (stacked) return <DashboardView dashboard={dashboard} stacked />
|
||||
|
||||
// The panel's own surface, scaled to fit. No dots: nothing is being
|
||||
// arranged here.
|
||||
return (
|
||||
<CanvasSurface dashboard={dashboard}>
|
||||
{() => <DashboardView dashboard={dashboard} />}
|
||||
</CanvasSurface>
|
||||
<div className="relative size-full">
|
||||
{stacked ? (
|
||||
<DashboardView dashboard={dashboard} stacked />
|
||||
) : (
|
||||
// The panel's own surface, scaled to fit. No dots: nothing is being
|
||||
// arranged here.
|
||||
<CanvasSurface dashboard={dashboard}>
|
||||
{() => <DashboardView dashboard={dashboard} />}
|
||||
</CanvasSurface>
|
||||
)}
|
||||
<LockNotice dashboard={dashboard} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* What a locked dashboard looks like, beyond controls that read as disabled.
|
||||
*
|
||||
* Without it a read-only panel is a panel whose buttons do nothing, which
|
||||
* reads as broken rather than as locked. Frosted chrome over content, like
|
||||
* every other floating surface, and it says the state in words — a glyph on
|
||||
* its own is not a label.
|
||||
*
|
||||
* Live, because `locked` may be driven by a flow: the notice appears and goes
|
||||
* with the lock rather than with the page load.
|
||||
*/
|
||||
function LockNotice({ dashboard }: { dashboard: Dashboard }) {
|
||||
// Read straight from the document rather than through `useLocked`: that
|
||||
// provider sits inside the grid, and this notice is beside it.
|
||||
const locked = useDashboardLocked(dashboard)
|
||||
if (!locked) return null
|
||||
return (
|
||||
<div
|
||||
// Announced rather than merely drawn: `locked` may be driven by a flow,
|
||||
// so the state can change under someone already looking at the page.
|
||||
aria-live="polite"
|
||||
data-testid="dashboard-locked"
|
||||
className="pointer-events-none absolute bottom-0 right-0 flex items-center gap-1.5 rounded-full border border-border bg-card/80 px-3 py-1.5 text-sm text-muted-foreground shadow-e2 backdrop-blur-md"
|
||||
>
|
||||
<Lock className="size-4" aria-hidden />
|
||||
Read-only
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user