Files
app/frontend/src/components/Dashboard/PanelSurface.tsx
T
stroblmeandClaude Opus 5 aa3e2e8b81 Stop a bound dashboard setting rebuilding every tile
A setting driven by a flow re-renders whatever reads it on every message.
`LookProvider` and `PaletteProvider` built a fresh context value each time,
and `CanvasSurface` called its render prop again, so a value on `theme`,
`palette`, `look`, `background`, `touch` or `locked` reconciled the whole
widget tree and re-ran every chart's `setData`.

The palette is now read as roles once per set of colours, the tokens and
both context values are memoised on it, and the canvas holds the tiles it
built while the callback and the scale stay put. `WidgetBody` is memoised
too: its two props are stable while the document is, so a grid rendered for
its own reasons no longer rebuilds every chart with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VfgHWr7AQDiLsTQVu7hpKB
2026-09-14 14:38:59 +02:00

86 lines
3.1 KiB
TypeScript

import { useCallback } from "react"
import type { Dashboard } from "@/components/Dashboard/DashboardView"
import {
CanvasSurface,
DashboardView,
} from "@/components/Dashboard/DashboardView"
import { useDashboardLocked } from "@/components/Dashboard/settings"
import { useUi } from "@/components/Dashboard/ui"
import { LookProvider } from "@/components/Dashboard/ui/core/look"
/**
* One dashboard filling whatever screen it landed on.
*
* 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,
stacked,
rail,
touch,
}: {
dashboard: Dashboard
/** A landscape arrangement scaled onto a phone comes out at about a fifth of
* its size, which reads as nothing at all. Stack it instead. */
stacked?: boolean
/** The way between this panel's dashboards, drawn on the canvas itself. */
rail?: React.ReactNode
/** Whether the screen this hangs on is touched. Stated by the panel route. */
touch?: boolean
}) {
const hasRail = Boolean(rail)
// Stable, so the canvas can keep the tiles it already built when one of the
// dashboard's own settings re-renders the route above this.
const canvas = useCallback(
() => <DashboardView dashboard={dashboard} rail={hasRail} />,
[dashboard, hasRail],
)
return (
<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} rail={rail} touch={touch}>
{canvas}
</CanvasSurface>
)}
{/* Outside the canvas, so it needs the look stated for it. */}
<LookProvider dashboard={dashboard} touch={touch}>
<LockNotice dashboard={dashboard} />
</LookProvider>
</div>
)
}
/**
* What a locked dashboard looks like, beyond controls that read as disabled.
*
* Shared with the editor's view mode, which shows the same locked dashboard
* from inside the app.
*
* 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.
*/
export function LockNotice({ dashboard }: { dashboard: Dashboard }) {
const { Notice } = useUi()
// 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 <Notice>Read-only</Notice>
}