Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -301,7 +301,7 @@ look's muted text colour.
|
||||
| Not wired | the problem, at 17, muted, in the middle; a ring type draws no ring |
|
||||
| Any other type | its rectangular body, without a card, from y = c − 64 to c + S/(2√2), x = c ± S/(2√2) (Knob: 53–307 × 116–307) |
|
||||
| Page dots | one per face, from two faces: dots 6 across on targets 16 apart (closer when more than 128 wide would be needed), centred at y = S − 12; the current one in the text colour, the others muted at half opacity |
|
||||
| Dashboard name | a pill centred at (c, c), 36 high: the icon 16 and the title at 15, medium, at most 220 wide, on the frosted surface; for 1000 ms |
|
||||
| Dashboard name | a pill centred at (c, c), 56 high and fully round, 20 padding either side: the icon 24, 10, and the title at 24, medium, cut with an ellipsis; at most 280 wide, on the look's card colour, opaque, with a hairline border in `--border` (no blur), the face visible around it; for 1000 ms |
|
||||
| Paging | faces slide sideways, dashboards up and down; see [Motion](#motion) |
|
||||
| Locked | controls do nothing and read as disabled; the ring at 0.45 opacity. **Read-only** in the gap line's place (centred at y = S − 28): a lock glyph 15 and the word at 15, 6 apart, muted. A face whose gap line is in use leads that line with the lock glyph instead. Nothing is drawn outside the circle |
|
||||
|
||||
@@ -330,9 +330,9 @@ dashboard's look's.
|
||||
| What | Moves | From → to | Timing | Input during it |
|
||||
|---|---|---|---|---|
|
||||
| Face paging (round) | the faces, side by side on a track | the track slides one face width S, the new face coming in from the side it lies on (the next from the right). A dot that skips faces brings its face in beside the one shown, so a turn is always one width | spring, from where the track is (a swipe's release speed carried on) | a new turn while one runs: the running one lands at once and the next starts |
|
||||
| Dashboard paging (round panel) | the dashboard, and beside it the next one's name | slides S up (the next, from below) or down (the previous, from above), the name pill centred on an otherwise empty face; once it has landed and the dashboard has loaded, the dashboard replaces its name in place. One opened by its address replaces the last at once | spring | further paging waits for the turn to land |
|
||||
| Dashboard paging (round panel) | the dashboard, and beside it the next one | slides S up (the next, from below) or down (the previous, from above), the next one drawn on the face it opens on; once landed, it is the dashboard shown, and its name pill shows for a moment. One opened by its address replaces the last at once | spring | further paging waits for the turn to land |
|
||||
| Page dot | the current dot and the last | muted at 0.5 opacity ↔ text colour at 1 | 200 ms, cubic-bezier(0.4, 0, 0.2, 1) | retargets |
|
||||
| Dashboard name | the pill | opacity 0 → 1, held 1000 ms, 1 → 0; on a dashboard a turn brought, where the name came in already, it starts at 1 | in 200 ms, out 150 ms, cubic-bezier(0.4, 0, 0.2, 1) | — |
|
||||
| Dashboard name | the pill, on landing on another dashboard | opacity 0 → 1, held 1000 ms, 1 → 0 | in 200 ms, out 150 ms, cubic-bezier(0.4, 0, 0.2, 1) | — |
|
||||
| Ring value set by a notch, a key or the engine | the value arc's end, the handle (slider, selector, colour) | from where it is drawn to the new fraction of the sweep | spring | each notch retargets from where it is, at the speed it has |
|
||||
| Ring value set by a drag | the same | follows the pointer exactly | none | — |
|
||||
| Hue across red | the colour handle | a change of more than half the sweep jumps to the other end | none | — |
|
||||
@@ -363,7 +363,8 @@ under the finger, as a tile view moves.
|
||||
|---|---|
|
||||
| Start | a press anywhere on the face but the ring band, which turns the ring instead |
|
||||
| Axis | none until the drag has gone 8 from where it began along either axis; then the longer of the two, kept until release |
|
||||
| Follow | the page moves with the drag one to one, and the page that way is drawn beside it, edge to edge, S away: the next face, or the next dashboard's name pill on an otherwise empty face |
|
||||
| Follow | the page moves with the drag one to one, and the page that way is drawn beside it, edge to edge, S away: the next face, or the next dashboard on the face it would open on (the one last shown there, else its first), with its live values |
|
||||
| Neighbours | the dashboards before and after the one shown are built in advance — loaded, drawn and fed live values — so the one a swipe reaches is complete from the first move. They take no input until they are the one shown |
|
||||
| Past the last | nothing beside it; the page moves 0.3 of the drag, at most 48 |
|
||||
| Release | turns if the drag went at least S/4 (90 on the Knob), or went at least 8 and was moving at least 500 a second towards the next page over its last 80 ms; otherwise goes back |
|
||||
| Turn, go back | the look's spring from where the page is, carrying the release speed; a device may use the look's cubic-bezier over its duration from where it is |
|
||||
|
||||
@@ -400,6 +400,7 @@ export function DashboardView({
|
||||
size={canvasOf(dashboard).width}
|
||||
onDashboard={onDashboard}
|
||||
around={around}
|
||||
editing={editing}
|
||||
/>
|
||||
) : (
|
||||
<WidgetGrid
|
||||
|
||||
@@ -22,7 +22,12 @@ import { resolveIcon } from "./icons"
|
||||
import { usePublish } from "./publish"
|
||||
import { dashboardQueryOptions } from "./queries"
|
||||
import { CentreReading, FaceTitle, hasGapLine, Ring } from "./RingWidget"
|
||||
import { useLocked, usePalette } from "./settings"
|
||||
import {
|
||||
LockedProvider,
|
||||
PaletteProvider,
|
||||
useLocked,
|
||||
usePalette,
|
||||
} from "./settings"
|
||||
import { useUi } from "./ui"
|
||||
import { arcPath } from "./ui/core/arc"
|
||||
import {
|
||||
@@ -206,47 +211,54 @@ function usePageDrag(
|
||||
/**
|
||||
* A panel's round dashboards, one at a time, paged up and down.
|
||||
*
|
||||
* Swiped, the dashboard follows the finger and the next one's name comes up
|
||||
* from below it (the previous one's down from above); let go far enough and
|
||||
* the turn completes, and the dashboard itself takes its name's place once it
|
||||
* has loaded. Shift+PageUp/PageDown turn the same way. One opened by its
|
||||
* address replaces the last at once.
|
||||
* The dashboards either side are drawn a screen above and below, ahead of
|
||||
* time and live, each on the face it would open on, so a swipe up or down
|
||||
* drags the next one in exactly as a sideways swipe drags the next face. Let
|
||||
* go far enough and the turn completes; Shift+PageUp/PageDown turn the same
|
||||
* way. One opened by its address replaces the last at once.
|
||||
*/
|
||||
export function RoundPanel({
|
||||
dashboard,
|
||||
size,
|
||||
onDashboard,
|
||||
around,
|
||||
editing,
|
||||
}: {
|
||||
dashboard: Dashboard
|
||||
size: number
|
||||
onDashboard?: (by: number) => void
|
||||
/** The panel's dashboards before and after this one. */
|
||||
around?: [string | undefined, string | undefined]
|
||||
/** Being arranged, so nothing is locked. */
|
||||
editing?: boolean
|
||||
}) {
|
||||
const { look } = useLook()
|
||||
const still = useReducedMotion()
|
||||
const spring = LOOK[look].spring
|
||||
const y = useMotionValue(0)
|
||||
// The dashboard held at the origin while a turn is on its way to the next,
|
||||
// the side the next one's name is on, whether the turn has landed, and the
|
||||
// last dashboard a turn brought, which has been named already.
|
||||
// where that one is and what it is called, and whether the turn has landed.
|
||||
const [held, setHeld] = useState<Dashboard | null>(null)
|
||||
const [beside, setBeside] = useState<{ by: 1 | -1; name: string } | null>(
|
||||
const [turning, setTurning] = useState<{ by: 1 | -1; name: string } | null>(
|
||||
null,
|
||||
)
|
||||
const [landed, setLanded] = useState(false)
|
||||
const [reached, setReached] = useState<string | null>(null)
|
||||
const shown = held ?? dashboard
|
||||
const name = (by: number) => around?.[by > 0 ? 1 : 0]
|
||||
const read = (of: string | undefined) => ({
|
||||
...dashboardQueryOptions(of ?? ""),
|
||||
enabled: Boolean(of),
|
||||
})
|
||||
const before = useQuery(read(around?.[0])).data
|
||||
const after = useQuery(read(around?.[1])).data
|
||||
// By the name it had when the turn began: the panel's neighbours are
|
||||
// counted from the new dashboard as soon as it is asked for.
|
||||
const target = useQuery(read(turning?.name)).data
|
||||
|
||||
const turn = (by: 1 | -1, speed = 0) => {
|
||||
const next = name(by)
|
||||
if (!next || !onDashboard || held) return
|
||||
setHeld(dashboard)
|
||||
// Its name as it was when the turn began: the panel's neighbours are
|
||||
// counted from the new dashboard as soon as it is asked for.
|
||||
setBeside({ by, name: next })
|
||||
setTurning({ by, name: next })
|
||||
setLanded(false)
|
||||
onDashboard(by)
|
||||
if (still) {
|
||||
@@ -265,90 +277,100 @@ export function RoundPanel({
|
||||
}).then(() => setLanded(true))
|
||||
}
|
||||
|
||||
// Landed, and the dashboard it went to has loaded: that one takes its
|
||||
// name's place, which it was drawn as, so nothing moves.
|
||||
// Landed, and the panel has moved on to it: it becomes the one at the
|
||||
// origin, which it is already drawn as, so nothing moves.
|
||||
useLayoutEffect(() => {
|
||||
if (!held || !landed || dashboard.name === held.name) return
|
||||
setHeld(null)
|
||||
setBeside(null)
|
||||
setTurning(null)
|
||||
setLanded(false)
|
||||
setReached(dashboard.name)
|
||||
y.jump(0)
|
||||
}, [held, landed, dashboard.name, y])
|
||||
|
||||
const rise: Axis = {
|
||||
open: (by) => Boolean(name(by) && onDashboard) && !held,
|
||||
drag: (offset, by) => {
|
||||
if (held) return
|
||||
const next = onDashboard ? name(by) : undefined
|
||||
if (next !== beside?.name) setBeside(next ? { by, name: next } : null)
|
||||
y.set(offset)
|
||||
drag: (offset) => {
|
||||
if (!held) y.set(offset)
|
||||
},
|
||||
release: (by, turnIt, speed) => {
|
||||
if (held) return
|
||||
if (turnIt) {
|
||||
turn(by, speed)
|
||||
return
|
||||
}
|
||||
if (still) {
|
||||
y.jump(0)
|
||||
setBeside(null)
|
||||
return
|
||||
}
|
||||
animate(y, 0, { ...spring, velocity: speed }).then(() => setBeside(null))
|
||||
if (turnIt) turn(by, speed)
|
||||
else if (still) y.jump(0)
|
||||
else animate(y, 0, { ...spring, velocity: speed })
|
||||
},
|
||||
}
|
||||
|
||||
// On the track by place: the one shown and its neighbours a screen either
|
||||
// side; during a turn, the one it left and the one it is going to.
|
||||
const pages: [Dashboard | undefined, number][] =
|
||||
held && turning
|
||||
? [
|
||||
[held, 0],
|
||||
[target, turning.by],
|
||||
]
|
||||
: [
|
||||
[before, -1],
|
||||
[dashboard, 0],
|
||||
[after, 1],
|
||||
]
|
||||
|
||||
return (
|
||||
<motion.div className="absolute inset-0" style={{ y }}>
|
||||
<RoundView
|
||||
// Its own face memory per dashboard, so a panel switching to another
|
||||
// starts that one where it was left.
|
||||
key={shown.name}
|
||||
dashboard={shown}
|
||||
size={size}
|
||||
onDashboard={onDashboard && ((by) => turn(by > 0 ? 1 : -1))}
|
||||
rise={rise}
|
||||
named={reached === shown.name}
|
||||
/>
|
||||
{beside ? (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 flex items-center justify-center"
|
||||
style={{ top: beside.by * size, height: size }}
|
||||
>
|
||||
<DashboardName name={beside.name} />
|
||||
</div>
|
||||
) : null}
|
||||
{pages.map(([page, at]) => {
|
||||
if (!page) return null
|
||||
const view = (
|
||||
<RoundView
|
||||
dashboard={page}
|
||||
size={size}
|
||||
peek={at !== 0}
|
||||
onDashboard={
|
||||
at === 0 && onDashboard
|
||||
? (by) => turn(by > 0 ? 1 : -1)
|
||||
: undefined
|
||||
}
|
||||
rise={at === 0 ? rise : undefined}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
// Keyed by name, so a neighbour becomes the one shown, and back,
|
||||
// without being drawn afresh: its own face memory and all.
|
||||
<div
|
||||
key={page.name}
|
||||
className="absolute inset-0"
|
||||
style={{ translate: `0 ${at * size}px` }}
|
||||
inert={at !== 0}
|
||||
>
|
||||
<PaletteProvider dashboard={page}>
|
||||
{editing ? (
|
||||
view
|
||||
) : (
|
||||
<LockedProvider dashboard={page}>{view}</LockedProvider>
|
||||
)}
|
||||
</PaletteProvider>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
/** A dashboard's icon and title, on the frosted surface: what a panel shows
|
||||
* of a dashboard it is turning to, and of one it has just landed on. */
|
||||
function NamePill({ dashboard }: { dashboard: Dashboard | undefined }) {
|
||||
const Glyph = resolveIcon(dashboard?.icon ?? "")
|
||||
/** A dashboard's icon and title: what a panel shows for a moment of a
|
||||
* dashboard it has just landed on. Opaque, so the face under it does not
|
||||
* read through the name. */
|
||||
function NamePill({ dashboard }: { dashboard: Dashboard }) {
|
||||
const Glyph = resolveIcon(dashboard.icon ?? "")
|
||||
return (
|
||||
<output
|
||||
data-testid="dashboard-announce"
|
||||
className="flex h-9 max-w-[220px] items-center gap-2 rounded-full border border-border bg-card/80 px-4 font-medium shadow-e2 backdrop-blur-md"
|
||||
style={{ fontSize: 15 }}
|
||||
className="flex h-14 max-w-[280px] items-center gap-2.5 rounded-full border border-border bg-card px-5 font-medium text-card-foreground shadow-e2"
|
||||
style={{ fontSize: 24 }}
|
||||
>
|
||||
{Glyph ? <Glyph className="size-4 shrink-0" aria-hidden /> : null}
|
||||
<span className="truncate">{dashboard?.title || dashboard?.name}</span>
|
||||
{Glyph ? <Glyph className="size-6 shrink-0" aria-hidden /> : null}
|
||||
<span className="truncate">{dashboard.title || dashboard.name}</span>
|
||||
</output>
|
||||
)
|
||||
}
|
||||
|
||||
/** The name of a dashboard not drawn yet, read for it — which is also what
|
||||
* has it loaded by the time a turn lands on it. */
|
||||
function DashboardName({ name }: { name: string }) {
|
||||
const { data } = useQuery({
|
||||
...dashboardQueryOptions(name),
|
||||
enabled: Boolean(name),
|
||||
})
|
||||
return <NamePill dashboard={data ?? ({ name } as Dashboard)} />
|
||||
}
|
||||
|
||||
/**
|
||||
* A round dashboard: one widget at a time, each drawn as the whole face.
|
||||
*
|
||||
@@ -358,7 +380,8 @@ function DashboardName({ name }: { name: string }) {
|
||||
* the panel's other dashboards (`rise`, `onDashboard`: a swipe or
|
||||
* Shift+PageUp/PageDown), and landing on one names it for a moment. Each
|
||||
* dashboard comes back to the face it was left on. The arrow keys stay the
|
||||
* ring's.
|
||||
* ring's. A `peek` is a neighbour drawn beside the one shown: it answers to
|
||||
* nothing until it becomes the one shown.
|
||||
*
|
||||
* Arranging, the editor says which face is shown (`face`, `onFace`) and a
|
||||
* press on a face that is not one of its controls picks it (`onPick`).
|
||||
@@ -372,7 +395,7 @@ export function RoundView({
|
||||
onPick,
|
||||
onDashboard,
|
||||
rise,
|
||||
named,
|
||||
peek,
|
||||
}: {
|
||||
dashboard: Dashboard
|
||||
/** The face's diameter: the canvas width. */
|
||||
@@ -385,9 +408,7 @@ export function RoundView({
|
||||
onDashboard?: (by: number) => void
|
||||
/** Swiping up and down, to the panel's other dashboards. */
|
||||
rise?: Axis
|
||||
/** Turned to, its name already shown on the way: it stays without
|
||||
* fading in again. */
|
||||
named?: boolean
|
||||
peek?: boolean
|
||||
}) {
|
||||
const widgets = dashboard.widgets ?? []
|
||||
const { look } = useLook()
|
||||
@@ -408,16 +429,15 @@ export function RoundView({
|
||||
// whether a drag has the track.
|
||||
const fling = useRef(0)
|
||||
const dragging = useRef(false)
|
||||
const [announce, setAnnounce] = useState(
|
||||
() =>
|
||||
Boolean(onDashboard) &&
|
||||
lastShown !== null &&
|
||||
lastShown !== dashboard.name,
|
||||
)
|
||||
const [announce, setAnnounce] = useState(false)
|
||||
|
||||
// Named for a moment when a panel lands on it from another — once it is
|
||||
// the one shown, not while it waits beside it.
|
||||
useEffect(() => {
|
||||
if (onDashboard) lastShown = dashboard.name
|
||||
}, [onDashboard, dashboard.name])
|
||||
if (!onDashboard || peek) return
|
||||
if (lastShown !== null && lastShown !== dashboard.name) setAnnounce(true)
|
||||
lastShown = dashboard.name
|
||||
}, [onDashboard, peek, dashboard.name])
|
||||
|
||||
useEffect(() => {
|
||||
if (!announce) return
|
||||
@@ -477,11 +497,12 @@ export function RoundView({
|
||||
onFace?.(next)
|
||||
}
|
||||
|
||||
const keys = !editing && !peek
|
||||
useShortcuts({
|
||||
pagedown: () => (editing ? false : page(index + 1)),
|
||||
pageup: () => (editing ? false : page(index - 1)),
|
||||
"shift+pagedown": () => (onDashboard && !editing ? onDashboard(1) : false),
|
||||
"shift+pageup": () => (onDashboard && !editing ? onDashboard(-1) : false),
|
||||
pagedown: () => (keys ? page(index + 1) : false),
|
||||
pageup: () => (keys ? page(index - 1) : false),
|
||||
"shift+pagedown": () => (onDashboard && keys ? onDashboard(1) : false),
|
||||
"shift+pageup": () => (onDashboard && keys ? onDashboard(-1) : false),
|
||||
})
|
||||
|
||||
const sideways: Axis = {
|
||||
@@ -629,7 +650,7 @@ export function RoundView({
|
||||
</nav>
|
||||
) : null}
|
||||
|
||||
<AnimatePresence initial={!named}>
|
||||
<AnimatePresence>
|
||||
{announce ? (
|
||||
<motion.div
|
||||
key="announce"
|
||||
|
||||
Reference in New Issue
Block a user