@octane-xplat/motion 0.7.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +31 -2
  2. package/UPSTREAM.md +295 -27
  3. package/dist/native/index.js +980 -112
  4. package/dist/native/octane-client-build.json +1 -1
  5. package/dist/web/index.js +1104 -166
  6. package/dist/web/octane-client-build.json +1 -1
  7. package/package.json +25 -16
  8. package/src/clock.ts +13 -1
  9. package/src/components.mobile.test.tsrx +58 -1
  10. package/src/components.tsrx +185 -39
  11. package/src/components.web.test.tsrx +236 -3
  12. package/src/controller.ts +122 -10
  13. package/src/delegation.test.ts +269 -0
  14. package/src/delegation.ts +29 -0
  15. package/src/delegation.web.test.ts +270 -0
  16. package/src/drag.mobile.test.ts +81 -0
  17. package/src/drag.test.ts +167 -0
  18. package/src/drag.ts +157 -0
  19. package/src/driver.ts +249 -0
  20. package/src/driver.web.ts +180 -0
  21. package/src/engine.test.ts +99 -0
  22. package/src/engine.ts +140 -18
  23. package/src/hooks.tsrx +53 -2
  24. package/src/host-types.ts +44 -1
  25. package/src/host.ts +39 -2
  26. package/src/host.web.ts +51 -10
  27. package/src/index.ts +18 -4
  28. package/src/index.web.ts +18 -4
  29. package/src/pan.ts +92 -0
  30. package/src/pan.web.ts +116 -0
  31. package/src/presence-input.ts +11 -4
  32. package/src/presence-input.web.ts +13 -4
  33. package/src/presence-state.ts +27 -7
  34. package/src/presence.tsrx +6 -7
  35. package/src/reduced-motion.ts +23 -4
  36. package/src/types.ts +100 -11
  37. package/src/value.ts +18 -8
  38. package/src/variants.test.ts +197 -0
  39. package/src/variants.ts +226 -0
  40. package/types/engine.d.ts +8 -2
  41. package/types/index.d.ts +34 -6
  42. package/types/types.d.ts +94 -11
  43. package/types/value.d.ts +7 -4
package/src/host.web.ts CHANGED
@@ -1,4 +1,7 @@
1
- import type { HostAdapter } from './host-types'
1
+ import { attachPan } from './pan.web'
2
+ import { delegatedRun } from './driver.web'
3
+ import type { DelegatedRequest, DelegatedRun, HostAdapter } from './host-types'
4
+ import type { Target } from './types'
2
5
  export function attachHost(node: HTMLElement): HostAdapter {
3
6
  const transform = node.style.transform
4
7
  const opacity = node.style.opacity
@@ -7,7 +10,18 @@ export function attachHost(node: HTMLElement): HostAdapter {
7
10
  throw new Error('motion: put existing CSS transforms on an outer container')
8
11
  }
9
12
 
13
+ const write = (values: Target) => {
14
+ if (values.opacity !== undefined) {
15
+ node.style.opacity = String(Math.min(1, Math.max(0, values.opacity)))
16
+ }
17
+
18
+ if (Object.keys(values).some((key) => key !== 'opacity')) {
19
+ node.style.transform = `translateX(${values.x ?? 0}px) translateY(${values.y ?? 0}px) scale(${values.scale ?? 1}) scaleX(${values.scaleX ?? 1}) scaleY(${values.scaleY ?? 1}) rotate(${values.rotate ?? 0}deg)`
20
+ }
21
+ }
22
+
10
23
  return {
24
+ pan: (axis, callbacks) => attachPan(node, axis, callbacks),
11
25
  read: () => ({
12
26
  opacity: Number(computed.opacity || 1),
13
27
  x: 0,
@@ -17,18 +31,45 @@ export function attachHost(node: HTMLElement): HostAdapter {
17
31
  scaleY: 1,
18
32
  rotate: 0,
19
33
  }),
20
- write(values) {
21
- if (values.opacity !== undefined) {
22
- node.style.opacity = String(Math.min(1, Math.max(0, values.opacity)))
23
- }
24
-
25
- if (Object.keys(values).some((key) => key !== 'opacity')) {
26
- node.style.transform = `translateX(${values.x ?? 0}px) translateY(${values.y ?? 0}px) scale(${values.scale ?? 1}) scaleX(${values.scaleX ?? 1}) scaleY(${values.scaleY ?? 1}) rotate(${values.rotate ?? 0}deg)`
27
- }
28
- },
34
+ write,
35
+ delegate: (req: DelegatedRequest): DelegatedRun | null => delegatedRun(node, req, write),
29
36
  restore() {
30
37
  node.style.transform = transform
31
38
  node.style.opacity = opacity
32
39
  },
40
+ gesture(kind, callbacks) {
41
+ if (kind === 'press') {
42
+ const down = (event: PointerEvent) => {
43
+ try {
44
+ // Preserve descendant click targets when this host wraps controls.
45
+ ;(event.target as Element | null)?.setPointerCapture?.(event.pointerId)
46
+ } catch (error) {
47
+ // Synthetic pointer dispatch has no active browser pointer.
48
+ if (event.isTrusted || (error as Error).name !== 'NotFoundError') {
49
+ throw error
50
+ }
51
+ }
52
+
53
+ callbacks.start()
54
+ }
55
+
56
+ const end = () => callbacks.end()
57
+ node.addEventListener('pointerdown', down)
58
+ node.addEventListener('pointerup', end)
59
+ node.addEventListener('pointercancel', end)
60
+ return () => {
61
+ node.removeEventListener('pointerdown', down)
62
+ node.removeEventListener('pointerup', end)
63
+ node.removeEventListener('pointercancel', end)
64
+ }
65
+ }
66
+
67
+ node.addEventListener('focus', callbacks.start)
68
+ node.addEventListener('blur', callbacks.end)
69
+ return () => {
70
+ node.removeEventListener('focus', callbacks.start)
71
+ node.removeEventListener('blur', callbacks.end)
72
+ }
73
+ },
33
74
  }
34
75
  }
package/src/index.ts CHANGED
@@ -1,8 +1,22 @@
1
- import { MotionView, MotionRow, MotionPressable } from './components.tsrx'
2
- /** Bounded motion hosts for shared xplat UI primitives. */
3
- export const motion = { View: MotionView, Row: MotionRow, Pressable: MotionPressable }
1
+ import { MotionView, MotionRow, MotionPressable, createMotion } from './components.tsrx'
2
+ /** Bounded motion hosts for shared xplat UI primitives; `motion.create` wraps
3
+ * any component that accepts `ref`/`style`/`children`. */
4
+ export const motion = {
5
+ View: MotionView,
6
+ Row: MotionRow,
7
+ Pressable: MotionPressable,
8
+ create: createMotion,
9
+ }
10
+
4
11
  export { MotionConfig, useReducedMotion } from './config.tsrx'
5
- export { useMotionValue, useTransform, useSpring, useMotionValueEvent } from './hooks.tsrx'
12
+ export {
13
+ useMotionValue,
14
+ useTransform,
15
+ useSpring,
16
+ useMotionValueEvent,
17
+ useAnimate,
18
+ } from './hooks.tsrx'
19
+
6
20
  export type { MotionValue, MotionValueEvents } from './value'
7
21
  export type { AnimationControls, AnimationResult } from './engine'
8
22
  export type * from './types'
package/src/index.web.ts CHANGED
@@ -1,8 +1,22 @@
1
- import { MotionView, MotionRow, MotionPressable } from './components.tsrx'
2
- /** Bounded motion hosts for shared xplat UI primitives. */
3
- export const motion = { View: MotionView, Row: MotionRow, Pressable: MotionPressable }
1
+ import { MotionView, MotionRow, MotionPressable, createMotion } from './components.tsrx'
2
+ /** Bounded motion hosts for shared xplat UI primitives; `motion.create` wraps
3
+ * any component that accepts `ref`/`style`/`children`. */
4
+ export const motion = {
5
+ View: MotionView,
6
+ Row: MotionRow,
7
+ Pressable: MotionPressable,
8
+ create: createMotion,
9
+ }
10
+
4
11
  export { MotionConfig, useReducedMotion } from './config.tsrx'
5
- export { useMotionValue, useTransform, useSpring, useMotionValueEvent } from './hooks.tsrx'
12
+ export {
13
+ useMotionValue,
14
+ useTransform,
15
+ useSpring,
16
+ useMotionValueEvent,
17
+ useAnimate,
18
+ } from './hooks.tsrx'
19
+
6
20
  export type { MotionValue, MotionValueEvents } from './value'
7
21
  export type { AnimationControls, AnimationResult } from './engine'
8
22
  export type * from './types'
package/src/pan.ts ADDED
@@ -0,0 +1,92 @@
1
+ import {
2
+ Manager,
3
+ HandlerType,
4
+ GestureState,
5
+ GestureHandlerStateEvent,
6
+ GestureHandlerTouchEvent,
7
+ } from '@nativescript-community/gesturehandler'
8
+
9
+ import { Utils } from '@nativescript/core'
10
+ import type { PanCallbacks } from './host-types'
11
+ import type { DragInfo } from './types'
12
+
13
+ // Separate tag range from the plugin's built-in gesture observers. Manager
14
+ // shares the registry; applications should reserve this range for motion.
15
+ let tag = 700000000
16
+
17
+ export function attachPan(node: any, axis: true | 'x' | 'y', callbacks: PanCallbacks) {
18
+ // 2.0.45 config setters take raw Android pixels (their converter is
19
+ // getter-only); payloads already convert back to DIP.
20
+ const androidHost = Boolean((globalThis as any).android)
21
+ const slop = androidHost ? Utils.layout.toDevicePixels(8) : 8
22
+ const handler = Manager.getInstance().createGestureHandler(HandlerType.PAN, tag++, {
23
+ shouldCancelWhenOutside: false,
24
+ // Android retains its radial touch-slop default alongside activeOffset.
25
+ // Disable that alternative trigger for single-axis arbitration.
26
+ ...(androidHost && axis !== true ? { minDist: 1e9 } : {}),
27
+ ...(axis === true
28
+ ? { minDist: slop }
29
+ : axis === 'x'
30
+ ? {
31
+ activeOffsetXStart: -slop,
32
+ activeOffsetXEnd: slop,
33
+ failOffsetYStart: -slop,
34
+ failOffsetYEnd: slop,
35
+ }
36
+ : {
37
+ activeOffsetYStart: -slop,
38
+ activeOffsetYEnd: slop,
39
+ failOffsetXStart: -slop,
40
+ failOffsetXEnd: slop,
41
+ }),
42
+ })
43
+
44
+ let active = false
45
+ let last = { x: 0, y: 0 }
46
+ const sample = (args: any, cancelled = false): DragInfo => {
47
+ const extra = args.data.extraData ?? {}
48
+ const offset = { x: extra.translationX ?? last.x, y: extra.translationY ?? last.y }
49
+ const delta = { x: offset.x - last.x, y: offset.y - last.y }
50
+ last = offset
51
+ return {
52
+ point: { x: extra.absoluteX ?? extra.x ?? 0, y: extra.absoluteY ?? extra.y ?? 0 },
53
+ delta,
54
+ offset,
55
+ velocity: { x: extra.velocityX ?? 0, y: extra.velocityY ?? 0 },
56
+ cancelled,
57
+ }
58
+ }
59
+
60
+ const state = (args: any) => {
61
+ const next = args.data.state
62
+ if (next === GestureState.ACTIVE && !active) {
63
+ last = { x: 0, y: 0 }
64
+ active = true
65
+ const info = sample(args)
66
+ callbacks.start(args, info)
67
+ callbacks.move(args, info)
68
+ } else if (
69
+ active &&
70
+ [GestureState.END, GestureState.CANCELLED, GestureState.FAILED].includes(next)
71
+ ) {
72
+ active = false
73
+ callbacks.end(args, sample(args, next !== GestureState.END))
74
+ }
75
+ }
76
+
77
+ const touch = (args: any) => {
78
+ if (active && args.data.state === GestureState.ACTIVE) {
79
+ callbacks.move(args, sample(args))
80
+ }
81
+ }
82
+
83
+ handler.on(GestureHandlerStateEvent, state)
84
+ handler.on(GestureHandlerTouchEvent, touch)
85
+ handler.attachToView(node)
86
+ return () => {
87
+ active = false
88
+ handler.off(GestureHandlerStateEvent, state)
89
+ handler.off(GestureHandlerTouchEvent, touch)
90
+ handler.detachFromView(node)
91
+ }
92
+ }
package/src/pan.web.ts ADDED
@@ -0,0 +1,116 @@
1
+ import type { PanCallbacks } from './host-types'
2
+ import type { DragInfo } from './types'
3
+
4
+ export function attachPan(node: HTMLElement, axis: true | 'x' | 'y', callbacks: PanCallbacks) {
5
+ const touchAction = node.style.touchAction
6
+ node.style.touchAction = axis === 'x' ? 'pan-y' : axis === 'y' ? 'pan-x' : 'none'
7
+ let pointer: number | undefined
8
+ let active = false
9
+ let start = { x: 0, y: 0 }
10
+ let last = start
11
+ let time = 0
12
+ let velocity = { x: 0, y: 0 }
13
+ let velocityTime = 0
14
+ const sample = (event: PointerEvent, cancelled = false): DragInfo => {
15
+ const point = cancelled ? last : { x: event.clientX, y: event.clientY }
16
+ const delta = { x: point.x - last.x, y: point.y - last.y }
17
+ const elapsed = event.timeStamp - time
18
+ if (elapsed > 0 && (delta.x || delta.y)) {
19
+ velocity = { x: (delta.x * 1000) / elapsed, y: (delta.y * 1000) / elapsed }
20
+ velocityTime = event.timeStamp
21
+ } else if (event.timeStamp - velocityTime > 100) {
22
+ velocity = { x: 0, y: 0 }
23
+ }
24
+
25
+ last = point
26
+ time = event.timeStamp
27
+ return {
28
+ point,
29
+ delta,
30
+ offset: { x: point.x - start.x, y: point.y - start.y },
31
+ velocity,
32
+ cancelled,
33
+ }
34
+ }
35
+
36
+ const down = (event: PointerEvent) => {
37
+ if (pointer !== undefined || event.button !== 0 || event.isPrimary === false) {
38
+ return
39
+ }
40
+
41
+ pointer = event.pointerId
42
+ start = last = { x: event.clientX, y: event.clientY }
43
+ time = velocityTime = event.timeStamp
44
+ velocity = { x: 0, y: 0 }
45
+ try {
46
+ node.setPointerCapture?.(event.pointerId)
47
+ } catch (error) {
48
+ // Synthetic pointer dispatch has no active browser pointer.
49
+ if (event.isTrusted || (error as Error).name !== 'NotFoundError') {
50
+ throw error
51
+ }
52
+ }
53
+ }
54
+
55
+ const move = (event: PointerEvent) => {
56
+ if (event.pointerId !== pointer) {
57
+ return
58
+ }
59
+
60
+ const info = sample(event)
61
+ if (!active) {
62
+ const primary = axis === 'y' ? info.offset.y : info.offset.x
63
+ const other = axis === 'y' ? info.offset.x : info.offset.y
64
+ if (axis !== true && Math.abs(other) > 8 && Math.abs(primary) <= 8) {
65
+ finish(event, true)
66
+ return
67
+ }
68
+
69
+ if ((axis === true ? Math.hypot(info.offset.x, info.offset.y) : Math.abs(primary)) <= 8) {
70
+ return
71
+ }
72
+
73
+ active = true
74
+ callbacks.start(event, info)
75
+ }
76
+
77
+ callbacks.move(event, info)
78
+ }
79
+
80
+ const finish = (event: PointerEvent, cancelled: boolean) => {
81
+ if (pointer === undefined || event.pointerId !== pointer) {
82
+ return
83
+ }
84
+
85
+ const id = pointer
86
+ pointer = undefined
87
+ if (active) {
88
+ active = false
89
+ callbacks.end(event, sample(event, cancelled))
90
+ }
91
+
92
+ if (node.hasPointerCapture?.(id)) {
93
+ node.releasePointerCapture(id)
94
+ }
95
+ }
96
+
97
+ const up = (event: PointerEvent) => finish(event, false)
98
+ const cancel = (event: PointerEvent) => finish(event, true)
99
+ node.addEventListener('pointerdown', down)
100
+ node.addEventListener('pointermove', move)
101
+ node.addEventListener('pointerup', up)
102
+ node.addEventListener('pointercancel', cancel)
103
+ node.addEventListener('lostpointercapture', cancel)
104
+ return () => {
105
+ node.removeEventListener('pointerdown', down)
106
+ node.removeEventListener('pointermove', move)
107
+ node.removeEventListener('pointerup', up)
108
+ node.removeEventListener('pointercancel', cancel)
109
+ node.removeEventListener('lostpointercapture', cancel)
110
+ if (pointer !== undefined && node.hasPointerCapture?.(pointer)) {
111
+ node.releasePointerCapture(pointer)
112
+ }
113
+
114
+ node.style.touchAction = touchAction
115
+ }
116
+ }
@@ -15,12 +15,19 @@ export function blockInput(node: any): () => void {
15
15
  }
16
16
 
17
17
  node.isUserInteractionEnabled = false
18
- if (node.ios) {node.ios.accessibilityElementsHidden = true}
18
+ if (node.ios) {
19
+ node.ios.accessibilityElementsHidden = true
20
+ }
21
+
19
22
  node.android?.setImportantForAccessibility?.(4) // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
20
23
  return () => {
21
24
  node.isUserInteractionEnabled = enabled
22
- if (node.ios) {node.ios.accessibilityElementsHidden = iosHidden}
23
- if (androidImportance !== undefined)
24
- {node.android?.setImportantForAccessibility?.(androidImportance)}
25
+ if (node.ios) {
26
+ node.ios.accessibilityElementsHidden = iosHidden
27
+ }
28
+
29
+ if (androidImportance !== undefined) {
30
+ node.android?.setImportantForAccessibility?.(androidImportance)
31
+ }
25
32
  }
26
33
  }
@@ -3,14 +3,23 @@ export function blockInput(node: HTMLElement): () => void {
3
3
  const hidden = node.getAttribute('aria-hidden')
4
4
  const pointerEvents = node.style.pointerEvents
5
5
  const focused = node.ownerDocument.activeElement as HTMLElement | null
6
- if (focused && node.contains(focused)) {focused.blur()}
6
+ if (focused && node.contains(focused)) {
7
+ focused.blur()
8
+ }
9
+
7
10
  node.setAttribute('inert', '')
8
11
  node.setAttribute('aria-hidden', 'true')
9
12
  node.style.pointerEvents = 'none'
10
13
  return () => {
11
- if (!inert) {node.removeAttribute('inert')}
12
- if (hidden === null) {node.removeAttribute('aria-hidden')}
13
- else {node.setAttribute('aria-hidden', hidden)}
14
+ if (!inert) {
15
+ node.removeAttribute('inert')
16
+ }
17
+
18
+ if (hidden === null) {
19
+ node.removeAttribute('aria-hidden')
20
+ } else {
21
+ node.setAttribute('aria-hidden', hidden)
22
+ }
14
23
 
15
24
  node.style.pointerEvents = pointerEvents
16
25
  }
@@ -20,7 +20,10 @@ export class PresenceState {
20
20
  ) {}
21
21
  register(member: PresenceMember): () => void {
22
22
  this.members.add(member)
23
- if (!this.present) {this.restart(member)}
23
+ if (!this.present) {
24
+ this.restart(member)
25
+ }
26
+
24
27
  return () => {
25
28
  this.members.delete(member)
26
29
  this.pending.delete(member)
@@ -28,26 +31,43 @@ export class PresenceState {
28
31
  }
29
32
  }
30
33
  setPresent(present: boolean) {
31
- if (this.disposed || present === this.present) {return}
34
+ if (this.disposed || present === this.present) {
35
+ return
36
+ }
37
+
32
38
  this.present = present
33
39
  ++this.generation
34
40
  this.removed = false
35
41
  this.pending.clear()
36
42
  if (present) {
37
- for (const member of this.members) {member.enter()}
43
+ for (const member of this.members) {
44
+ member.enter()
45
+ }
38
46
  } else {
39
- for (const member of this.members) {this.restart(member)}
47
+ for (const member of this.members) {
48
+ this.restart(member)
49
+ }
50
+
40
51
  this.check()
41
52
  }
42
53
  }
43
54
  restart(member: PresenceMember) {
44
- if (this.disposed || this.present || !this.members.has(member)) {return}
55
+ if (this.disposed || this.present || !this.members.has(member)) {
56
+ return
57
+ }
58
+
45
59
  const token = ++this.sequence
46
60
  this.pending.set(member, token)
47
61
  void member.exit().then((result) => {
48
- if (this.disposed || this.present || this.pending.get(member) !== token) {return}
62
+ if (this.disposed || this.present || this.pending.get(member) !== token) {
63
+ return
64
+ }
65
+
49
66
  // Replacement is owned by the next restart. Cancellation cannot remove a live subtree.
50
- if (result !== 'finished') {return}
67
+ if (result !== 'finished') {
68
+ return
69
+ }
70
+
51
71
  this.pending.delete(member)
52
72
  this.check()
53
73
  })
package/src/presence.tsrx CHANGED
@@ -11,13 +11,13 @@ export function Presence(props: PresenceProps) @{
11
11
  const [retained, setRetained] = useState(props.present)
12
12
  const latest = useRef(props)
13
13
  latest.current = props
14
- const ref = useRef<PresenceState | null>(null)
15
- if (!ref.current) ref.current = new PresenceState(props.present, () => {
14
+ const stateRef = useRef<PresenceState | null>(null)
15
+ if (!stateRef.current) stateRef.current = new PresenceState(props.present, () => {
16
16
  setRetained(false)
17
17
  latest.current.onExitComplete?.()
18
18
  })
19
19
 
20
- const state = ref.current
20
+ const state = stateRef.current
21
21
  const host = useRef<any>(null)
22
22
  const restoreInput = useRef<(() => void) | null>(null)
23
23
  const syncInput = () => {
@@ -38,14 +38,13 @@ export function Presence(props: PresenceProps) @{
38
38
  host.current = null
39
39
  }, [])
40
40
 
41
- const { present, onExitComplete, children, bind, ...viewProps } = props
41
+ const { present, onExitComplete, children, ref, ...viewProps } = props
42
42
 
43
43
  @if (props.present || retained) {
44
- <View {...viewProps} bind={(node: any) => {
44
+ <View {...viewProps} ref={[(node: any) => {
45
45
  if (host.current !== node) { restoreInput.current?.(); restoreInput.current = null; host.current = node }
46
46
  syncInput()
47
- bind?.(node)
48
- }}>
47
+ }, ref]}>
49
48
  <PresenceContext value={state}>{children}</PresenceContext>
50
49
  </View>
51
50
  }
@@ -1,4 +1,4 @@
1
- import { Application, Utils } from '@nativescript/core'
1
+ import { Application, Device, Utils } from '@nativescript/core'
2
2
 
3
3
  export function readReducedMotion(): boolean {
4
4
  const platform = globalThis as any
@@ -6,7 +6,19 @@ export function readReducedMotion(): boolean {
6
6
  return Boolean(platform.UIAccessibilityIsReduceMotionEnabled?.())
7
7
  }
8
8
 
9
- const context = Utils.android.getApplicationContext()
9
+ if (!platform.android) {
10
+ return false
11
+ }
12
+
13
+ if (
14
+ Number(Device.sdkVersion) >= 26 &&
15
+ platform.android.animation?.ValueAnimator?.areAnimatorsEnabled?.() === false
16
+ ) {
17
+ return true
18
+ }
19
+
20
+ // `Utils.android` is a {} stub in the pinned Windows core build.
21
+ const context = (Utils.android as any).getApplicationContext()
10
22
  return (
11
23
  platform.android.provider.Settings.Global.getFloat(
12
24
  context.getContentResolver(),
@@ -16,9 +28,16 @@ export function readReducedMotion(): boolean {
16
28
  )
17
29
  }
18
30
 
19
- // Poll only while subscribed; this also detects Android developer-setting changes
20
- // without owning a Java ContentObserver class. Resume refreshes immediately.
31
+ // iOS posts a notification on preference change; Android has no event, so it
32
+ // polls only while subscribed (also catches developer-setting changes) and
33
+ // refreshes on resume.
21
34
  export function observeReducedMotion(notify: () => void): () => void {
35
+ if (Application.ios) {
36
+ const name = (globalThis as any).UIAccessibilityReduceMotionStatusDidChangeNotification
37
+ const observer = Application.ios.addNotificationObserver(name, notify)
38
+ return () => Application.ios.removeNotificationObserver(observer, name)
39
+ }
40
+
22
41
  let previous = readReducedMotion()
23
42
  const check = () => {
24
43
  const next = readReducedMotion()