Changing dashboard kept the grid mounted, so every new tile rose at once. The grid is drawn afresh per dashboard and its tiles come in row by row: from below, top row first, for one further down the rail, and from above, bottom row first, for one further up, as a round panel's dashboards slide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
140 lines
5.3 KiB
TypeScript
140 lines
5.3 KiB
TypeScript
import { useCallback } from "react"
|
|
|
|
import type { Dashboard } from "@/components/Dashboard/DashboardView"
|
|
import {
|
|
CanvasGround,
|
|
CanvasSurface,
|
|
DashboardView,
|
|
isRound,
|
|
} from "@/components/Dashboard/DashboardView"
|
|
import { useDashboardLocked } from "@/components/Dashboard/settings"
|
|
import { useUi } from "@/components/Dashboard/ui"
|
|
import { LookProvider } from "@/components/Dashboard/ui/core/look"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
/**
|
|
* 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,
|
|
onDashboard,
|
|
around,
|
|
turned,
|
|
}: {
|
|
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. Never a round
|
|
* one: a knob's face is about a phone's width already. */
|
|
stacked?: boolean
|
|
/** The way between this panel's dashboards, drawn on the canvas itself, or
|
|
* along the bottom of the screen when stacked. */
|
|
rail?: React.ReactNode
|
|
/** Whether the screen this hangs on is touched. Stated by the panel route. */
|
|
touch?: boolean
|
|
/** A round dashboard has no rail: it is swiped up and down to the panel's
|
|
* others instead, whose names are `around` it. */
|
|
onDashboard?: (by: number) => void
|
|
around?: [string | undefined, string | undefined]
|
|
/** Which way along the rail this dashboard was reached: down (1) or up
|
|
* (-1). */
|
|
turned?: number
|
|
}) {
|
|
const hasRail = Boolean(rail)
|
|
const stack = stacked && !isRound(dashboard)
|
|
// 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}
|
|
onDashboard={onDashboard}
|
|
around={around}
|
|
turned={turned}
|
|
/>
|
|
),
|
|
[dashboard, hasRail, onDashboard, around, turned],
|
|
)
|
|
return (
|
|
<div className="relative size-full">
|
|
{stack ? (
|
|
// 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}>
|
|
{/* The look's ground, held to the screen while the stack scrolls
|
|
over it, as it stays put under a canvas. */}
|
|
<div className="pointer-events-none fixed inset-0">
|
|
<CanvasGround dashboard={dashboard} />
|
|
</div>
|
|
{/* Padded here rather than by the route: the stack overflows this
|
|
box, so only padding inside the overflow clears the rail. */}
|
|
<div className={cn("relative", rail && "pb-24")}>
|
|
<DashboardView dashboard={dashboard} stacked turned={turned} />
|
|
</div>
|
|
{rail ? (
|
|
// No column to spare on a phone, so the rail is a row floating
|
|
// over the bottom edge.
|
|
<div className="pointer-events-none fixed inset-x-4 bottom-[calc(1rem+env(safe-area-inset-bottom))] z-10 flex justify-center">
|
|
{rail}
|
|
</div>
|
|
) : null}
|
|
</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}>
|
|
{stack && rail ? (
|
|
// Lifted clear of the rail floating along the bottom.
|
|
<div className="pointer-events-none absolute inset-x-0 top-0 bottom-20">
|
|
<LockNotice dashboard={dashboard} />
|
|
</div>
|
|
) : (
|
|
<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)
|
|
// A round face says it in its own gap (`RoundView`): a corner outside the
|
|
// circle is under a knob's bezel.
|
|
if (!locked || isRound(dashboard)) return null
|
|
return <Notice>Read-only</Notice>
|
|
}
|