mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-28 07:15:10 +00:00
fix(ui): honor reduced motion across timeline motion
Make spring-driven layout changes snap when reduced motion is enabled and disable the remaining timeline and explore animations that were still easing despite the system preference.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { createEffect, on, type JSX, onMount, onCleanup } from "solid-js"
|
||||
import { animate, tunableSpringValue, type AnimationPlaybackControls, GROW_SPRING, type SpringConfig } from "./motion"
|
||||
import { prefersReducedMotion } from "../hooks/use-reduced-motion"
|
||||
|
||||
export interface GrowBoxProps {
|
||||
children: JSX.Element
|
||||
@@ -48,6 +49,7 @@ export interface GrowBoxProps {
|
||||
* Used for timeline turns, assistant part groups, and user messages.
|
||||
*/
|
||||
export function GrowBox(props: GrowBoxProps) {
|
||||
const reduce = prefersReducedMotion
|
||||
const spring = () => props.spring ?? GROW_SPRING
|
||||
const toggleSpring = () => props.toggleSpring ?? spring()
|
||||
let mode: "mount" | "toggle" = "mount"
|
||||
@@ -83,7 +85,8 @@ export function GrowBox(props: GrowBoxProps) {
|
||||
const edgeIdle = () => Math.max(0, props.edgeIdle ?? 320)
|
||||
const edgeFade = () => Math.max(0.05, props.edgeFade ?? 0.24)
|
||||
const edgeRise = () => Math.max(0.05, props.edgeRise ?? 0.2)
|
||||
const edgeReady = () => props.animate !== false && open() && edge() && edgeHeight() > 0
|
||||
const animated = () => props.animate !== false && !reduce()
|
||||
const edgeReady = () => animated() && open() && edge() && edgeHeight() > 0
|
||||
|
||||
const stopEdgeTimer = () => {
|
||||
if (edgeTimer === undefined) return
|
||||
@@ -99,7 +102,7 @@ export function GrowBox(props: GrowBoxProps) {
|
||||
}
|
||||
edgeAnim?.stop()
|
||||
edgeAnim = undefined
|
||||
if (instant) {
|
||||
if (instant || reduce()) {
|
||||
edgeRef.style.opacity = "0"
|
||||
edgeOn = false
|
||||
return
|
||||
@@ -124,6 +127,11 @@ export function GrowBox(props: GrowBoxProps) {
|
||||
const showEdge = () => {
|
||||
stopEdgeTimer()
|
||||
if (!edgeRef) return
|
||||
if (reduce()) {
|
||||
edgeRef.style.opacity = `${edgeOpacity()}`
|
||||
edgeOn = true
|
||||
return
|
||||
}
|
||||
if (edgeOn && edgeAnim === undefined) {
|
||||
edgeRef.style.opacity = `${edgeOpacity()}`
|
||||
return
|
||||
@@ -175,6 +183,10 @@ export function GrowBox(props: GrowBoxProps) {
|
||||
|
||||
const fadeBodyIn = (nextMode: "mount" | "toggle" = "mount") => {
|
||||
if (props.fade === false || !body) return
|
||||
if (reduce()) {
|
||||
clearBody()
|
||||
return
|
||||
}
|
||||
hideBody()
|
||||
fadeAnim?.stop()
|
||||
fadeAnim = animate(body, { opacity: 1, filter: "blur(0px)" }, nextMode === "toggle" ? toggleSpring() : spring())
|
||||
@@ -185,6 +197,9 @@ export function GrowBox(props: GrowBoxProps) {
|
||||
}
|
||||
|
||||
const setInstant = (visible: boolean) => {
|
||||
const next = visible ? targetHeight() : 0
|
||||
springTarget = next
|
||||
height.jump(next)
|
||||
root!.style.height = visible ? "" : "0px"
|
||||
root!.style.overflow = visible ? "" : "clip"
|
||||
hideEdge(true)
|
||||
@@ -207,6 +222,18 @@ export function GrowBox(props: GrowBoxProps) {
|
||||
const setHeight = (nextMode: "mount" | "toggle" = "mount") => {
|
||||
if (!root || !open()) return
|
||||
const next = targetHeight()
|
||||
if (reduce()) {
|
||||
springTarget = next
|
||||
height.jump(next)
|
||||
if (props.autoHeight === false || watch()) {
|
||||
root.style.height = `${next}px`
|
||||
root.style.overflow = next > 0 ? "visible" : "clip"
|
||||
return
|
||||
}
|
||||
root.style.height = "auto"
|
||||
root.style.overflow = next > 0 ? "visible" : "clip"
|
||||
return
|
||||
}
|
||||
if (next === springTarget) return
|
||||
const prev = currentHeight()
|
||||
if (Math.abs(next - prev) < 1) {
|
||||
@@ -266,7 +293,7 @@ export function GrowBox(props: GrowBoxProps) {
|
||||
offChange()
|
||||
})
|
||||
|
||||
if (!props.animate) {
|
||||
if (!animated()) {
|
||||
setInstant(open())
|
||||
return
|
||||
}
|
||||
@@ -310,7 +337,7 @@ export function GrowBox(props: GrowBoxProps) {
|
||||
(value) => {
|
||||
if (value === undefined) return
|
||||
if (!root || !body) return
|
||||
if (!animateToggle()) {
|
||||
if (!animateToggle() || reduce()) {
|
||||
setInstant(value)
|
||||
return
|
||||
}
|
||||
@@ -342,7 +369,7 @@ export function GrowBox(props: GrowBoxProps) {
|
||||
createEffect(() => {
|
||||
if (!edgeRef) return
|
||||
edgeRef.style.height = `${edgeHeight()}px`
|
||||
if (props.animate === false || !open() || edgeHeight() <= 0) {
|
||||
if (!animated() || !open() || edgeHeight() <= 0) {
|
||||
hideEdge(true)
|
||||
return
|
||||
}
|
||||
@@ -350,6 +377,14 @@ export function GrowBox(props: GrowBoxProps) {
|
||||
hideEdge()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!root || !body) return
|
||||
if (!reduce()) return
|
||||
fadeAnim?.stop()
|
||||
edgeAnim?.stop()
|
||||
setInstant(open())
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
stopEdgeTimer()
|
||||
if (mountFrame !== undefined) cancelAnimationFrame(mountFrame)
|
||||
|
||||
@@ -895,11 +895,12 @@ function ContextToolRollingResults(props: { parts: ToolPart[]; pending: boolean
|
||||
const wiped = new Set<string>()
|
||||
const [mounted, setMounted] = createSignal(false)
|
||||
onMount(() => setMounted(true))
|
||||
const reduce = prefersReducedMotion
|
||||
const show = () => mounted() && props.pending
|
||||
const opacity = useSpring(() => (show() ? 1 : 0), GROW_SPRING)
|
||||
const blur = useSpring(() => (show() ? 0 : 2), GROW_SPRING)
|
||||
return (
|
||||
<div style={{ opacity: opacity(), filter: `blur(${blur()}px)` }}>
|
||||
<div style={{ opacity: reduce() ? (show() ? 1 : 0) : opacity(), filter: `blur(${reduce() ? 0 : blur()}px)` }}>
|
||||
<RollingResults
|
||||
items={props.parts}
|
||||
rows={3}
|
||||
@@ -922,6 +923,7 @@ function ContextToolRollingResults(props: { parts: ToolPart[]; pending: boolean
|
||||
if (!el || !d) return
|
||||
if (wiped.has(k)) return
|
||||
wiped.add(k)
|
||||
if (reduce()) return
|
||||
el.style.maskImage = WIPE_MASK
|
||||
el.style.webkitMaskImage = WIPE_MASK
|
||||
el.style.maskSize = "240% 100%"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { attachSpring, motionValue } from "motion"
|
||||
import type { SpringOptions } from "motion"
|
||||
import { createEffect, createSignal, onCleanup } from "solid-js"
|
||||
import { prefersReducedMotion } from "../hooks/use-reduced-motion"
|
||||
|
||||
type Opt = Pick<SpringOptions, "visualDuration" | "bounce" | "stiffness" | "damping" | "mass" | "velocity">
|
||||
const eq = (a: Opt | undefined, b: Opt | undefined) =>
|
||||
@@ -13,24 +14,41 @@ const eq = (a: Opt | undefined, b: Opt | undefined) =>
|
||||
|
||||
export function useSpring(target: () => number, options?: Opt | (() => Opt)) {
|
||||
const read = () => (typeof options === "function" ? options() : options)
|
||||
const reduce = prefersReducedMotion
|
||||
const [value, setValue] = createSignal(target())
|
||||
const source = motionValue(value())
|
||||
const spring = motionValue(value())
|
||||
let config = read()
|
||||
let stop = attachSpring(spring, source, config)
|
||||
let reduced = reduce()
|
||||
let stop = reduced ? () => {} : attachSpring(spring, source, config)
|
||||
let off = spring.on("change", (next) => setValue(next))
|
||||
|
||||
createEffect(() => {
|
||||
source.set(target())
|
||||
const next = target()
|
||||
if (reduced) {
|
||||
source.set(next)
|
||||
spring.set(next)
|
||||
setValue(next)
|
||||
return
|
||||
}
|
||||
source.set(next)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!options) return
|
||||
const next = read()
|
||||
if (eq(config, next)) return
|
||||
const skip = reduce()
|
||||
if (eq(config, next) && reduced === skip) return
|
||||
config = next
|
||||
reduced = skip
|
||||
stop()
|
||||
stop = attachSpring(spring, source, next)
|
||||
stop = skip ? () => {} : attachSpring(spring, source, next)
|
||||
if (skip) {
|
||||
const value = target()
|
||||
source.set(value)
|
||||
spring.set(value)
|
||||
setValue(value)
|
||||
return
|
||||
}
|
||||
setValue(spring.get())
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user