Files
app/frontend/src/components/Dashboard/PanelSurface.tsx
T
stroblme 57fd3d1b2c Collapse the button-variant fork and the canvas's duplicate work
LoadingButton kept its own copy of buttonVariants, which had drifted past
`brand` and the icon sizes; it imports Button's now, so the Connect /
Re-pair button can show the loader instead of swapping its own label.

DashboardView mounted a second LookProvider inside the one CanvasSurface
already gives it and dropped `touch` on the way, so every widget on a
touch panel read `useLook().touch` as false. The provider is now mounted
once, by whatever owns the canvas root.

useCanvasRoot resolved the palette twice per render — once for the theme
class, once for the tokens — so useDashboardTheme takes the roles the
caller already has.

The bar assertions in widgets.spec.ts are scoped to the Load tile rather
than the page, which held only because the unbound spare bar draws no
rows.
2026-09-14 15:29:40 +02:00

91 lines
3.4 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 ? (
// Nothing scales a stack, so there is no canvas element here to state
// the look on: this branch mounts what `CanvasSurface` mounts for the
// other one, touch included.
<LookProvider dashboard={dashboard} touch={touch}>
<DashboardView dashboard={dashboard} stacked />
</LookProvider>
) : (
// 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>
}