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