@octane-xplat/motion 0.7.3 → 0.9.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.
package/UPSTREAM.md CHANGED
@@ -6,16 +6,16 @@ inspected from Octane main on 2026-09-29. Its UPSTREAM.md pins Motion 12.42.2
6
6
  `motion-dom@12.42.2` and bundles its pure numeric generators and interpolation.
7
7
  The adapter is original code; it does not copy upstream's DOM host factory.
8
8
 
9
- | Contract | Upstream reference under packages/motion | Xplat implementation / evidence |
10
- | ------------------------------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
11
- | Initial and changing targets, equal destinations | src/index.ts; tests/conformance/rerender.test.ts, effects.test.ts | Compiled wrappers over UI; components.web.test.tsrx |
12
- | Numeric spring/tween samples | Motion's motion-dom animation/generators | Reused generators; engine.test.ts compares irregular-time samples |
13
- | Stable value, subscriptions, style replacement | src/useMotionValue.ts; tests/conformance/motionValue.test.ts | Numeric adapter with native clock; components.web.test.tsrx |
14
- | Spring set versus jump; follow source | src/useSpring.ts; tests/conformance/useSpring.test.ts | Owned spring interception and cleanup; hook tests |
15
- | Derived numeric values | src/useTransform.ts; tests/conformance/useTransform.test.ts | Upstream interpolation, direct subscriptions; hook tests |
16
- | Config and reduced motion | src/context.ts; tests/conformance/reducedMotionConfig.test.ts | Context inheritance; transforms settle immediately, opacity may animate |
17
- | Exit lifecycle | src/index.ts; tests/conformance/exit.test.ts | Deliberate live-subtree retention on both leaves; presence.web.test.tsrx and native presence test |
18
- | Retained hosts on native | Not a DOM binding concern | components.mobile.test.tsrx uses the universal object driver |
9
+ | Contract | Upstream reference under packages/motion | Xplat implementation / evidence |
10
+ | ------------------------------------------------ | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | Initial and changing targets, equal destinations | src/index.ts; tests/conformance/rerender.test.ts, effects.test.ts | Compiled wrappers over UI; components.web.test.tsrx |
12
+ | Numeric spring/tween samples | Motion's motion-dom animation/generators | Reused generators; engine.test.ts compares irregular-time samples |
13
+ | Stable value, subscriptions, style replacement | src/useMotionValue.ts; tests/conformance/motionValue.test.ts | Numeric adapter with native clock; components.web.test.tsrx |
14
+ | Spring set versus jump; follow source | src/useSpring.ts; tests/conformance/useSpring.test.ts | Owned spring interception and cleanup; hook tests |
15
+ | Derived numeric values | src/useTransform.ts; tests/conformance/useTransform.test.ts | Upstream interpolation, direct subscriptions; hook tests |
16
+ | Config and reduced motion | src/context.ts; tests/conformance/reducedMotionConfig.test.ts | Context inheritance; spring transforms settle immediately on live preference changes while opacity can keep animating (`components.web.test.tsrx`, `engine.test.ts`) |
17
+ | Exit lifecycle | src/index.ts; tests/conformance/exit.test.ts | Deliberate live-subtree retention on both leaves; presence.web.test.tsrx and native presence test |
18
+ | Retained hosts on native | Not a DOM binding concern | components.mobile.test.tsrx uses the universal object driver |
19
19
 
20
20
  Source links: [Octane motion](https://github.com/octanejs/octane/tree/main/packages/motion),
21
21
  [upstream ledger](https://github.com/octanejs/octane/blob/main/packages/motion/UPSTREAM.md),
@@ -83,7 +83,7 @@ switch value before and after its Settings tap. A corrected rerun was blocked
83
83
  when the phone re-locked.
84
84
 
85
85
  Physical Android CPH2551 evidence covers app launch and target retarget only;
86
- the handset is currently disconnected. The regular mobile app now builds and
86
+ ADB sees the handset, but its keyguard is currently locked. The regular mobile app now builds and
87
87
  mounts on the iOS 26.5 simulator using NativeScript's `NSObject.extend()` API
88
88
  for the auth-session presentation delegate. The Android build also passes with
89
89
  the iOS delegate behind the `NSObject` runtime guard. Motion-specific simulator,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "7088f63b-f8ad-4e65-b418-39f3f911ecdb",
3
+ "buildId": "f3ab3f25-9a1e-4436-9723-a402e32f5c04",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "a2aeed68-461b-4f57-8879-b03abd73845d",
3
+ "buildId": "fcb6bc74-25e4-4f8e-af58-42140062b9cd",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
package/package.json CHANGED
@@ -1,26 +1,26 @@
1
1
  {
2
2
  "name": "@octane-xplat/motion",
3
- "version": "0.7.3",
3
+ "version": "0.9.0",
4
4
  "description": "Declarative motion and retained presence for Octane on web, iOS and Android",
5
5
  "keywords": [
6
- "octane",
7
- "xplat",
8
- "cross-platform",
6
+ "android",
9
7
  "animation",
8
+ "cross-platform",
9
+ "ios",
10
10
  "motion",
11
- "transitions",
12
- "spring",
13
- "presence",
14
11
  "nativescript",
15
- "ios",
16
- "android",
17
- "web"
12
+ "octane",
13
+ "presence",
14
+ "spring",
15
+ "transitions",
16
+ "web",
17
+ "xplat"
18
18
  ],
19
- "license": "MIT",
20
19
  "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/motion",
20
+ "license": "MIT",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "git+https://github.com/octane-xplat/octane-xplat.git",
23
+ "url": "https://github.com/octane-xplat/octane-xplat",
24
24
  "directory": "packages/motion"
25
25
  },
26
26
  "files": [
@@ -43,15 +43,16 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@octane-xplat/ui": "0.7.3",
46
+ "@octane-xplat/ui": "0.9.0",
47
47
  "motion-dom": "12.42.2"
48
48
  },
49
49
  "devDependencies": {
50
- "@nativescript-community/octane": "0.2.1",
50
+ "@nativescript-community/octane": "0.2.4",
51
51
  "@nativescript/core": "9.1.2",
52
52
  "@octanejs/vite-plugin": "0.1.61",
53
53
  "jsdom": "30.1.1",
54
54
  "octane": "0.6.3",
55
+ "tsrx-typegen": "0.2.1",
55
56
  "typescript": "5.9.3",
56
57
  "vite": "8.3.0",
57
58
  "vitest": "5.0.1"
@@ -70,7 +71,10 @@
70
71
  }
71
72
  },
72
73
  "scripts": {
73
- "build": "vite build && vite build --mode native && tsc -p tsconfig.types.json",
74
- "test": "vitest run"
74
+ "build": "vite build && vite build --mode native && pnpm typegen",
75
+ "test": "vitest run",
76
+ "typegen": "tsc -p tsconfig.types.json",
77
+ "test:packed": "node tests/packed-consumer.mjs",
78
+ "pack:check": "tsrx-typegen --pack-check"
75
79
  }
76
80
  }
@@ -38,7 +38,6 @@ describe('native compiled hosts', () => {
38
38
  })
39
39
  })
40
40
 
41
- import { vi } from 'vitest'
42
41
  import { useEffect } from 'octane'
43
42
  import { Presence } from './index'
44
43
  let nativeCleanups = 0
@@ -16,7 +16,7 @@ function Values(props: any) @{
16
16
  }
17
17
 
18
18
  function Reduced() @{
19
- <MotionConfig reducedMotion="always"><motion.Pressable id="reduced" initial={{ x: 0 }} animate={{ x: 100 }} transition={{ duration: 10 }} /></MotionConfig>
19
+ <MotionConfig reducedMotion="always"><motion.Pressable id="reduced" initial={{ x: 0 }} animate={{ x: 100 }} transition={{ type: 'spring' }} /></MotionConfig>
20
20
  }
21
21
 
22
22
  function mount(component: any) {
@@ -121,7 +121,7 @@ it('retargets from a render-time signal read', () => {
121
121
  })
122
122
 
123
123
  function UserReduced() @{
124
- <MotionConfig reducedMotion="user"><motion.View id="user-reduced" animate={{ x: 100 }} transition={{ duration: 10 }} /></MotionConfig>
124
+ <MotionConfig reducedMotion="user"><motion.View id="user-reduced" animate={{ x: 100 }} transition={{ type: 'spring' }} /></MotionConfig>
125
125
  }
126
126
 
127
127
  it('observes live system reduced-motion changes', () => {
@@ -123,4 +123,17 @@ describe('portable playback', () => {
123
123
  expect(await result).toBe('replaced')
124
124
  expect(time.count()).toBe(0)
125
125
  })
126
+
127
+ it('reduces positional springs immediately while preserving opacity motion', async () => {
128
+ const time = fakeClock()
129
+ const controller = new Controller(time.clock)
130
+ const result = controller.animate({ x: 100, opacity: 0 }, { type: 'spring' }, true)
131
+
132
+ expect(controller.snapshot()).toMatchObject({ x: 100, opacity: 1 })
133
+ expect(time.count()).toBe(1)
134
+ time.advance(10000)
135
+ expect(await result).toBe('finished')
136
+ expect(controller.snapshot()).toEqual({ x: 100, opacity: 0 })
137
+ controller.destroy()
138
+ })
126
139
  })
package/src/engine.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { keyframes, spring } from 'motion-dom'
2
- import type { Clock } from './clock-types'
3
- import type { Transition } from './types'
2
+ import type { Clock } from './clock-types.js'
3
+ import type { Transition } from './types.js'
4
4
 
5
5
  /** Terminal status; cancellation never masquerades as completion. */
6
6
  export type AnimationResult = 'finished' | 'cancelled' | 'replaced'
@@ -12,10 +12,10 @@ export interface AnimationControls {
12
12
 
13
13
  export function validateTransition(t: Transition): void {
14
14
  if (t.type === 'spring' && (t.duration !== undefined || t.ease !== undefined)) {
15
- throw new Error('motion: physical springs do not accept duration or ease')
16
- }
15
+ throw new Error('motion: physical springs do not accept duration or ease')
16
+ }
17
17
 
18
- const allowed = [
18
+ const allowed = [
19
19
  'type',
20
20
  'duration',
21
21
  'delay',
@@ -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/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { View, Row, Pressable } from '@octane-xplat/ui'
2
- import type { MotionValue } from './value'
2
+ import type { MotionValue } from './value.js'
3
3
 
4
4
  type ViewProps = Parameters<typeof View>[0]
5
5
  type RowProps = Parameters<typeof Row>[0]
package/src/value.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { Clock } from './clock-types'
2
- import type { Transition } from './types'
3
- import { runAnimation, validateTransition, type AnimationControls } from './engine'
1
+ import type { Clock } from './clock-types.js'
2
+ import type { Transition } from './types.js'
3
+ import { runAnimation, validateTransition, type AnimationControls } from './engine.js'
4
4
 
5
5
  /** Events supported by numeric motion values. */
6
6
  export interface MotionValueEvents {
package/types/engine.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Clock } from './clock-types';
2
- import type { Transition } from './types';
1
+ import type { Clock } from './clock-types.js';
2
+ import type { Transition } from './types.js';
3
3
  /** Terminal status; cancellation never masquerades as completion. */
4
4
  export type AnimationResult = 'finished' | 'cancelled' | 'replaced';
5
5
  /** A cancellable animation whose finished promise always settles. */
package/types/index.d.ts CHANGED
@@ -5,14 +5,16 @@ import type {
5
5
  MotionPressableProps,
6
6
  MotionConfigProps,
7
7
  Transition,
8
- } from './types'
9
- import type { MotionValue, MotionValueEvents } from './value'
8
+ } from './types.js'
9
+
10
+ import type { MotionValue, MotionValueEvents } from './value.js'
10
11
  /** Bounded motion hosts for shared xplat UI primitives. */
11
12
  export declare const motion: {
12
13
  View: UniversalComponent<MotionViewProps>
13
14
  Row: UniversalComponent<MotionRowProps>
14
15
  Pressable: UniversalComponent<MotionPressableProps>
15
16
  }
17
+
16
18
  /** Inherit transition and reduced-motion defaults within this root. */
17
19
  export declare const MotionConfig: UniversalComponent<MotionConfigProps>
18
20
  /** Observe the live system reduced-motion preference. */
@@ -25,6 +27,7 @@ export declare function useMotionValueEvent<K extends keyof MotionValueEvents>(
25
27
  event: K,
26
28
  callback: MotionValueEvents[K],
27
29
  ): void
30
+
28
31
  /** Derive a numeric value by mapping ranges or transforming source samples. */
29
32
  export declare function useTransform(
30
33
  source: MotionValue,
@@ -32,22 +35,26 @@ export declare function useTransform(
32
35
  output: number[],
33
36
  options?: { clamp?: boolean },
34
37
  ): MotionValue
38
+
35
39
  export declare function useTransform(
36
40
  source: MotionValue,
37
41
  transform: (value: number) => number,
38
42
  ): MotionValue
43
+
39
44
  export declare function useTransform(
40
45
  source: MotionValue[],
41
46
  transform: (values: number[]) => number,
42
47
  ): MotionValue
48
+
43
49
  /** Spring toward set targets or a source MotionValue; jump snaps. */
44
50
  export declare function useSpring(
45
51
  source: number | MotionValue,
46
52
  options?: Omit<Transition, 'type' | 'duration' | 'ease' | 'delay'>,
47
53
  ): MotionValue
54
+
48
55
  export type { MotionValue, MotionValueEvents }
49
- export type { AnimationControls, AnimationResult } from './engine'
50
- export type * from './types'
56
+ export type { AnimationControls, AnimationResult } from './engine.js'
57
+ export type * from './types.js'
51
58
 
52
59
  /** Retain live children through exit; removing the boundary disposes immediately. */
53
- export declare const Presence: UniversalComponent<import('./types').PresenceProps>
60
+ export declare const Presence: UniversalComponent<import('./types.js').PresenceProps>
package/types/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { View, Row, Pressable } from '@octane-xplat/ui';
2
- import type { MotionValue } from './value';
2
+ import type { MotionValue } from './value.js';
3
3
  type ViewProps = Parameters<typeof View>[0];
4
4
  type RowProps = Parameters<typeof Row>[0];
5
5
  type PressableProps = Parameters<typeof Pressable>[0];
package/types/value.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { Clock } from './clock-types';
2
- import type { Transition } from './types';
3
- import { type AnimationControls } from './engine';
1
+ import type { Clock } from './clock-types.js';
2
+ import type { Transition } from './types.js';
3
+ import { type AnimationControls } from './engine.js';
4
4
  /** Events supported by numeric motion values. */
5
5
  export interface MotionValueEvents {
6
6
  change: (value: number) => void;