diff --git a/docs/interface/dashboards.md b/docs/interface/dashboards.md index bb159e4..6f41cc0 100644 --- a/docs/interface/dashboards.md +++ b/docs/interface/dashboards.md @@ -347,7 +347,7 @@ dashboard's look's. | Value face, Value and Gauge tiles | the number | counts to the new value, written to its precision | spring | retargets | | Slider tile set elsewhere | the fill's end, the handle, the reading's dot | from where it is drawn to the new fraction | spring | a drag or a key moves it at once | | Gauge and Bar tiles | the value arc, each bar's fill | from where it is drawn to the new fraction | spring | retargets | -| Dashboard change (rectangular panel) | every tile | the old dashboard goes at once; each new tile opacity 0 → 1, y +8 → 0 | spring | — | +| Dashboard change (rectangular panel) | every tile | the old dashboard goes at once. The new tiles come in row by row, 30 ms apart, each opacity 0 → 1 and y +8 → 0 (Glass: +12, and scale 0.96 → 1): from below, top row first, for a dashboard further down the rail, and from above (y −8 → 0), bottom row first, for one further up | spring | — | | Rail | the current entry's marker | slides to the new entry | spring | retargets | With reduced motion asked for, nothing travels on its own: a page under the diff --git a/frontend/src/components/Dashboard/DashboardView.tsx b/frontend/src/components/Dashboard/DashboardView.tsx index 4495289..fc91f27 100644 --- a/frontend/src/components/Dashboard/DashboardView.tsx +++ b/frontend/src/components/Dashboard/DashboardView.tsx @@ -393,6 +393,7 @@ export function DashboardView({ editing, onDashboard, around, + turned, }: { dashboard: Dashboard renderWidget?: (widget: WidgetDef) => React.ReactNode @@ -407,6 +408,9 @@ export function DashboardView({ * previous (-1), and their names. */ onDashboard?: (by: number) => void around?: [string | undefined, string | undefined] + /** Which way along a panel's rail it was reached: down (1) or up (-1). + * Its tiles come in from that side. */ + turned?: number }) { const all = widgetsOf(dashboard) const round = isRound(dashboard) @@ -424,15 +428,15 @@ export function DashboardView({ ) } - // Stacked, the arrangement becomes a reading order, so it follows the rows - // the panel shows rather than the order widgets happened to be added in. - const widgets = stacked - ? [...all].sort((a, b) => { - const left = placement(a) - const right = placement(b) - return (left.y ?? 0) - (right.y ?? 0) || (left.x ?? 0) - (right.x ?? 0) - }) - : all + // In the rows the panel shows rather than the order widgets happened to be + // added in: the reading order a stack follows, and the order tiles arrive + // in. A grid places each one itself, and one nobody arranged has them all + // at 0,0, which the sort leaves as they were. + const widgets = [...all].sort((a, b) => { + const left = placement(a) + const right = placement(b) + return (left.y ?? 0) - (right.y ?? 0) || (left.x ?? 0) - (right.x ?? 0) + }) const columns = columnsOf(dashboard) const grid = ( @@ -449,11 +453,15 @@ export function DashboardView({ /> ) : ( )} @@ -480,6 +488,7 @@ function WidgetGrid({ columns, stacked, rail, + from, renderWidget, }: { dashboard: Dashboard @@ -487,6 +496,8 @@ function WidgetGrid({ columns: number stacked?: boolean rail?: boolean + /** The side the tiles come in from: below (1) or above (-1). */ + from: number renderWidget?: (widget: WidgetDef) => React.ReactNode }) { const { Frame } = useUi() @@ -496,6 +507,7 @@ function WidgetGrid({ className={cn("widget-grid", stacked && "widget-stacked")} data-placed={isPlaced(widgets) || undefined} variants={gridStagger} + custom={from} initial="hidden" animate="visible" style={ @@ -513,6 +525,7 @@ function WidgetGrid({ style={widgetStyle(widget, columns)} className="widget-cell" variants={LOOK[look].enter} + custom={from} transition={LOOK[look].spring} > {renderWidget ? ( diff --git a/frontend/src/components/Dashboard/PanelSurface.tsx b/frontend/src/components/Dashboard/PanelSurface.tsx index 39677f6..d30b2cc 100644 --- a/frontend/src/components/Dashboard/PanelSurface.tsx +++ b/frontend/src/components/Dashboard/PanelSurface.tsx @@ -30,6 +30,7 @@ export function PanelSurface({ touch, onDashboard, around, + turned, }: { dashboard: Dashboard /** A landscape arrangement scaled onto a phone comes out at about a fifth of @@ -45,6 +46,9 @@ export function PanelSurface({ * 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) @@ -57,9 +61,10 @@ export function PanelSurface({ rail={hasRail} onDashboard={onDashboard} around={around} + turned={turned} /> ), - [dashboard, hasRail, onDashboard, around], + [dashboard, hasRail, onDashboard, around, turned], ) return (
@@ -76,7 +81,7 @@ export function PanelSurface({ {/* Padded here rather than by the route: the stack overflows this box, so only padding inside the overflow clears the rail. */}
- +
{rail ? ( // No column to spare on a phone, so the rail is a row floating diff --git a/frontend/src/components/Dashboard/ui/core/motion.ts b/frontend/src/components/Dashboard/ui/core/motion.ts index cf70814..eb05953 100644 --- a/frontend/src/components/Dashboard/ui/core/motion.ts +++ b/frontend/src/components/Dashboard/ui/core/motion.ts @@ -21,7 +21,8 @@ import type { Look } from "../../settings" type LookMotion = { spring: Transition - /** How a widget arrives on the canvas. */ + /** How a widget arrives on the canvas. `custom` is the side it comes from, + * as `gridStagger` takes it. */ enter: Variants /** Seconds for one round of the ambient ground; 0 draws none. */ drift: number @@ -34,7 +35,7 @@ export const LOOK: Record = { // the way the rest of the product does. spring: { type: "spring", stiffness: 420, damping: 34, mass: 1 }, enter: { - hidden: { opacity: 0, y: 8 }, + hidden: (from = 1) => ({ opacity: 0, y: 8 * from }), visible: { opacity: 1, y: 0 }, }, drift: 0, @@ -42,7 +43,7 @@ export const LOOK: Record = { material: { spring: { type: "spring", stiffness: 550, damping: 38, mass: 1 }, enter: { - hidden: { opacity: 0, y: 8 }, + hidden: (from = 1) => ({ opacity: 0, y: 8 * from }), visible: { opacity: 1, y: 0 }, }, drift: 0, @@ -50,15 +51,23 @@ export const LOOK: Record = { glass: { spring: { type: "spring", stiffness: 320, damping: 22, mass: 0.8 }, enter: { - hidden: { opacity: 0, y: 12, scale: 0.96 }, + hidden: (from = 1) => ({ opacity: 0, y: 12 * from, scale: 0.96 }), visible: { opacity: 1, y: 0, scale: 1 }, }, drift: 46, }, } -/** Widgets arrive as a page rather than all at once. */ +/** + * Widgets arrive as a page rather than all at once, row by row. + * + * `custom` is the side they come from: below (1), as a round panel's next + * dashboard rises, or above (-1) for one further up a panel's rail, which + * brings the last row in first. + */ export const gridStagger: Variants = { hidden: {}, - visible: { transition: { staggerChildren: 0.03 } }, + visible: (from = 1) => ({ + transition: { staggerChildren: 0.03, staggerDirection: from < 0 ? -1 : 1 }, + }), } diff --git a/frontend/src/routes/panel.$id.tsx b/frontend/src/routes/panel.$id.tsx index 3030e8a..eaa3cde 100644 --- a/frontend/src/routes/panel.$id.tsx +++ b/frontend/src/routes/panel.$id.tsx @@ -1,6 +1,6 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query" import { createFileRoute, redirect } from "@tanstack/react-router" -import { useCallback, useMemo } from "react" +import { useCallback, useMemo, useState } from "react" import type { Dashboard } from "@/components/Dashboard/DashboardView" import { PanelRail } from "@/components/Dashboard/PanelRail" @@ -84,6 +84,16 @@ function PanelRoute() { }, [panel, current, navigate], ) + // Which way along the rail the dashboard shown was reached, so its tiles + // come in from that side: further down it (1) from below, as a round + // panel's next dashboard rises, and further up (-1) from above. + const shown = dashboard?.name + const [turned, setTurned] = useState({ name: shown, by: 1 }) + if (shown !== turned.name) { + const by = + dashboards.indexOf(shown ?? "") - dashboards.indexOf(turned.name ?? "") + setTurned({ name: shown, by: by < 0 ? -1 : 1 }) + } // Said by the panel rather than by the dashboard: this is the screen, and // whether it is touched or pointed at is a fact about the screen. A document // hanging on a hallway tablet and on a desk browser is one document either @@ -131,6 +141,7 @@ function PanelRoute() { dashboard={dashboard as Dashboard} stacked={stacked} touch={touch} + turned={turned.by} onDashboard={rail && round ? step : undefined} around={rail && round ? around : undefined} // Drawn on the panel rather than beside it: a screen showing four