@symbiote-native/engine 0.1.7 → 0.3.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 (87) hide show
  1. package/README.md +17 -10
  2. package/build/action-sheet-ios/index.js +4 -1
  3. package/build/alert/index.ios.js +10 -0
  4. package/build/animated/animations/composition.js +3 -1
  5. package/build/animated/animations/spring-config.js +4 -1
  6. package/build/animated/animations/spring.js +23 -9
  7. package/build/animated/animations/timing.js +3 -2
  8. package/build/animated/bezier.js +2 -1
  9. package/build/animated/color.d.ts +2 -8
  10. package/build/animated/color.js +8 -47
  11. package/build/animated/easing.js +0 -8
  12. package/build/animated/event.js +7 -3
  13. package/build/animated/graph.d.ts +13 -5
  14. package/build/animated/graph.js +92 -13
  15. package/build/animated/index.d.ts +3 -3
  16. package/build/animated/index.js +3 -3
  17. package/build/animated/interpolation.js +8 -3
  18. package/build/animated/leaf-lifecycle.d.ts +7 -0
  19. package/build/animated/leaf-lifecycle.js +185 -0
  20. package/build/animated/mock.js +1 -1
  21. package/build/animated/native/native-animated.js +2 -1
  22. package/build/animated/operators.js +27 -6
  23. package/build/animated/props.js +1 -1
  24. package/build/animated/rgba.d.ts +8 -0
  25. package/build/animated/rgba.js +70 -0
  26. package/build/animated/style.js +13 -3
  27. package/build/animated/value-xy.d.ts +8 -5
  28. package/build/animated/value-xy.js +4 -1
  29. package/build/animated/value.js +12 -7
  30. package/build/appearance/index.js +4 -1
  31. package/build/commit.d.ts +13 -0
  32. package/build/commit.js +146 -14
  33. package/build/debug.d.ts +1 -1
  34. package/build/debug.js +10 -3
  35. package/build/events/index.js +19 -10
  36. package/build/fabric-props.js +8 -2
  37. package/build/image-loader.js +3 -2
  38. package/build/index.d.ts +23 -19
  39. package/build/index.js +19 -11
  40. package/build/interaction-manager/index.js +2 -1
  41. package/build/layout-animation/index.js +12 -5
  42. package/build/native-events.js +2 -1
  43. package/build/native-modules/index.js +3 -1
  44. package/build/node.d.ts +25 -0
  45. package/build/node.js +168 -2
  46. package/build/pan-responder/index.js +20 -9
  47. package/build/permissions-android/index.js +3 -1
  48. package/build/platform/index.android.d.ts +1 -1
  49. package/build/platform/index.android.js +1 -1
  50. package/build/platform/index.ios.d.ts +1 -1
  51. package/build/platform-color/index.js +3 -1
  52. package/build/post-commit.d.ts +1 -0
  53. package/build/post-commit.js +6 -0
  54. package/build/process-background-image/index.js +30 -9
  55. package/build/process-filter.js +11 -4
  56. package/build/process-font-variant.js +0 -1
  57. package/build/process-transform/index.js +13 -19
  58. package/build/registry.js +10 -2
  59. package/build/report-error.d.ts +5 -0
  60. package/build/report-error.js +63 -0
  61. package/build/share/index.android.js +1 -1
  62. package/build/share/index.ios.js +1 -1
  63. package/build/status-bar/index.android.js +8 -10
  64. package/build/status-bar/index.ios.js +1 -1
  65. package/build/style-registry/index.d.ts +9 -2
  66. package/build/style-registry/index.js +149 -95
  67. package/build/style-registry/scope.d.ts +15 -1
  68. package/build/style-registry/scope.js +29 -27
  69. package/build/style-sheet/index.js +4 -2
  70. package/build/styles.d.ts +11 -1
  71. package/build/surface.d.ts +1 -1
  72. package/build/surface.js +7 -1
  73. package/build/tags.d.ts +1 -0
  74. package/build/tags.js +31 -1
  75. package/build/touch-history.js +9 -2
  76. package/build/vibration/index.android.js +2 -3
  77. package/build/vibration/index.ios.js +1 -1
  78. package/build/view-config.js +14 -2
  79. package/package.json +2 -2
  80. package/build/animated/interpolation-node.d.ts +0 -15
  81. package/build/animated/interpolation-node.js +0 -58
  82. package/build/style-registry/scope/index.d.ts +0 -3
  83. package/build/style-registry/scope/index.js +0 -41
  84. package/build/touch-history/index.d.ts +0 -23
  85. package/build/touch-history/index.js +0 -150
  86. package/build/type-guards/index.d.ts +0 -4
  87. package/build/type-guards/index.js +0 -15
package/README.md CHANGED
@@ -9,7 +9,7 @@ current one, clones only what changed, and calls `completeRoot` — the persiste
9
9
  dance Fabric requires, done **once**, for every framework.
10
10
 
11
11
  > New to SymbioteNative? The [root README](../../README.md) has the architecture and the one fact it
12
- > rests on — React is just *one client* of `nativeFabricUIManager`. This package is what sits
12
+ > rests on — React is just _one client_ of `nativeFabricUIManager`. This package is what sits
13
13
  > between every adapter and that native slot.
14
14
 
15
15
  ---
@@ -48,17 +48,24 @@ The entire surface a renderer seam drives:
48
48
 
49
49
  ```ts
50
50
  import {
51
- createElement, createRawText, createAnchor,
52
- appendChild, insertBefore, removeChild,
53
- routeProp, setEventListener, setProp, setText,
51
+ createElement,
52
+ createRawText,
53
+ createAnchor,
54
+ appendChild,
55
+ insertBefore,
56
+ removeChild,
57
+ routeProp,
58
+ setEventListener,
59
+ setProp,
60
+ setText,
54
61
  } from '@symbiote-native/engine';
55
62
 
56
- const node = createElement('RCTView'); // component IS the Fabric view name
63
+ const node = createElement('RCTView'); // component IS the Fabric view name
57
64
  const text = createRawText('Hello');
58
65
  appendChild(node, text);
59
- routeProp(node, 'onPress', () => {}); // ← the flat-bag entry point (React/Vue/Solid):
60
- // decides event-vs-prop via the ViewConfig,
61
- // NOT by the "onX" naming convention
66
+ routeProp(node, 'onPress', () => {}); // ← the flat-bag entry point (React/Vue/Solid):
67
+ // decides event-vs-prop via the ViewConfig,
68
+ // NOT by the "onX" naming convention
62
69
  ```
63
70
 
64
71
  `routeProp` is the one call a flat-bag adapter should route every prop through — a **structural**
@@ -72,7 +79,7 @@ import { createSurface } from '@symbiote-native/engine';
72
79
 
73
80
  const surface = createSurface(rootTag);
74
81
  surface.appendChild(root, node);
75
- surface.commit(); // synchronous — for a framework that already batches (React)
82
+ surface.commit(); // synchronous — for a framework that already batches (React)
76
83
  // surface.requestCommit(); // microtask-coalesced — for reactive frameworks (Vue/Svelte/Angular)
77
84
  ```
78
85
 
@@ -98,7 +105,7 @@ before a tag is guaranteed to exist.
98
105
  - **The style pipeline** — `flattenStyle`, the CSS-style processors RN itself runs in JS
99
106
  (`processBoxShadow`, `processFilter`, `processTransform`, `processTransformOrigin`,
100
107
  `processAspectRatio`, `processFontVariant`, `processBackgroundImage`), and the runtime
101
- **class-name registry** (`registerStyles` / `resolveClassName` / `scopeClassName`) that
108
+ **class-name registry** (`registerRules` / `resolveClassName` / `renameClassTokens`) that
102
109
  `@symbiote-native/css-parser`'s build-time output resolves against — shared by every adapter's
103
110
  `class` / `className` / `addClass` prop path.
104
111
  - **`AppRegistry` core** (`createAppRegistry`) — registry bookkeeping + headless-task plumbing;
@@ -41,7 +41,10 @@ export const ActionSheetIOS = {
41
41
  else if (typeof destructiveButtonIndex === 'number') {
42
42
  destructiveButtonIndices = [destructiveButtonIndex];
43
43
  }
44
- const nativeOptions = { ...remainingOptions, destructiveButtonIndices };
44
+ const nativeOptions = {
45
+ ...remainingOptions,
46
+ destructiveButtonIndices,
47
+ };
45
48
  manager.showActionSheetWithOptions(nativeOptions, buttonIndex => {
46
49
  dlog(`ActionSheetIOS callback buttonIndex=${buttonIndex}`);
47
50
  callback(buttonIndex);
@@ -43,6 +43,16 @@ function prompt(title, message, callbackOrButtons, type = 'plain-text', defaultV
43
43
  if (btn.isPreferred) {
44
44
  preferredButtonKey = String(index);
45
45
  }
46
+ // UPSTREAM-BUG(react-native): Libraries/Alert/Alert.js:173 - a TRAILING button with no text
47
+ // is left out of the native buttons array while its onPress still occupies callbacks[index],
48
+ // so that handler can never fire: native has no id to return for a button it was never told
49
+ // about. Only the last button, only when textless, which reads more like leftover
50
+ // length - 1 bookkeeping than an intended "omit a blank trailing button" affordance.
51
+ // Ported for parity; do NOT fix without recording a deliberate divergence.
52
+ //
53
+ // The `!== undefined` here IS a deliberate divergence from upstream's truthy `btn.text`:
54
+ // an explicit `text: ''` is a caller asking for a blank label, not an omission, and this
55
+ // codebase treats falsy-vs-absent as distinct on principle. Upstream drops that button.
46
56
  if (btn.text !== undefined || index < callbackOrButtons.length - 1) {
47
57
  buttons.push({ [index]: btn.text ?? '' });
48
58
  }
@@ -211,7 +211,9 @@ export function loop(animation, config = {}) {
211
211
  return;
212
212
  }
213
213
  const restart = (result = { finished: true }) => {
214
- if (isFinished || iterationsSoFar === iterations || result.finished === false) {
214
+ if (isFinished ||
215
+ iterationsSoFar === iterations ||
216
+ result.finished === false) {
215
217
  callback?.(result);
216
218
  }
217
219
  else {
@@ -34,7 +34,10 @@ export function fromBouncinessAndSpeed(bounciness, speed) {
34
34
  return 0.000044 * Math.pow(x, 3) - 0.006 * Math.pow(x, 2) + 0.36 * x + 2;
35
35
  }
36
36
  function b3Friction3(x) {
37
- return 0.00000045 * Math.pow(x, 3) - 0.000332 * Math.pow(x, 2) + 0.1078 * x + 5.84;
37
+ return (0.00000045 * Math.pow(x, 3) -
38
+ 0.000332 * Math.pow(x, 2) +
39
+ 0.1078 * x +
40
+ 5.84);
38
41
  }
39
42
  function b3Nobounce(tension) {
40
43
  if (tension <= 18)
@@ -6,10 +6,12 @@
6
6
  // continuous. The native-config export is dropped.
7
7
  import { dlog } from '../../debug.js';
8
8
  import { BaseAnimation } from './base.js';
9
- import { cancelFrame, clearTimer, requestFrame, setTimer } from './raf.js';
10
- import { fromBouncinessAndSpeed, fromOrigamiTensionAndFriction } from './spring-config.js';
9
+ import { cancelFrame, clearTimer, requestFrame, setTimer, } from './raf.js';
10
+ import { fromBouncinessAndSpeed, fromOrigamiTensionAndFriction, } from './spring-config.js';
11
11
  function resolveStiffnessDampingMass(config) {
12
- if (config.stiffness !== undefined || config.damping !== undefined || config.mass !== undefined) {
12
+ if (config.stiffness !== undefined ||
13
+ config.damping !== undefined ||
14
+ config.mass !== undefined) {
13
15
  return {
14
16
  stiffness: config.stiffness ?? 100,
15
17
  damping: config.damping ?? 10,
@@ -18,10 +20,18 @@ function resolveStiffnessDampingMass(config) {
18
20
  }
19
21
  if (config.bounciness !== undefined || config.speed !== undefined) {
20
22
  const springConfig = fromBouncinessAndSpeed(config.bounciness ?? 8, config.speed ?? 12);
21
- return { stiffness: springConfig.stiffness, damping: springConfig.damping, mass: 1 };
23
+ return {
24
+ stiffness: springConfig.stiffness,
25
+ damping: springConfig.damping,
26
+ mass: 1,
27
+ };
22
28
  }
23
29
  const springConfig = fromOrigamiTensionAndFriction(config.tension ?? 40, config.friction ?? 7);
24
- return { stiffness: springConfig.stiffness, damping: springConfig.damping, mass: 1 };
30
+ return {
31
+ stiffness: springConfig.stiffness,
32
+ damping: springConfig.damping,
33
+ mass: 1,
34
+ };
25
35
  }
26
36
  export class SpringAnimation extends BaseAnimation {
27
37
  overshootClamping;
@@ -141,7 +151,8 @@ export class SpringAnimation extends BaseAnimation {
141
151
  position =
142
152
  this.toValue -
143
153
  envelope *
144
- (((v0 + zeta * omega0 * x0) / omega1) * Math.sin(omega1 * t) + x0 * Math.cos(omega1 * t));
154
+ (((v0 + zeta * omega0 * x0) / omega1) * Math.sin(omega1 * t) +
155
+ x0 * Math.cos(omega1 * t));
145
156
  velocity =
146
157
  zeta *
147
158
  omega0 *
@@ -149,13 +160,15 @@ export class SpringAnimation extends BaseAnimation {
149
160
  ((Math.sin(omega1 * t) * (v0 + zeta * omega0 * x0)) / omega1 +
150
161
  x0 * Math.cos(omega1 * t)) -
151
162
  envelope *
152
- (Math.cos(omega1 * t) * (v0 + zeta * omega0 * x0) - omega1 * x0 * Math.sin(omega1 * t));
163
+ (Math.cos(omega1 * t) * (v0 + zeta * omega0 * x0) -
164
+ omega1 * x0 * Math.sin(omega1 * t));
153
165
  }
154
166
  else {
155
167
  // Critically damped.
156
168
  const envelope = Math.exp(-omega0 * t);
157
169
  position = this.toValue - envelope * (x0 + (v0 + omega0 * x0) * t);
158
- velocity = envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0));
170
+ velocity =
171
+ envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0));
159
172
  }
160
173
  this.lastTime = now;
161
174
  this.lastPosition = position;
@@ -177,7 +190,8 @@ export class SpringAnimation extends BaseAnimation {
177
190
  const isVelocity = Math.abs(velocity) <= this.restSpeedThreshold;
178
191
  let isDisplacement = true;
179
192
  if (this.stiffness !== 0) {
180
- isDisplacement = Math.abs(this.toValue - position) <= this.restDisplacementThreshold;
193
+ isDisplacement =
194
+ Math.abs(this.toValue - position) <= this.restDisplacementThreshold;
181
195
  }
182
196
  if (isOvershooting || (isVelocity && isDisplacement)) {
183
197
  if (this.stiffness !== 0) {
@@ -5,7 +5,7 @@
5
5
  import { Easing } from '../easing.js';
6
6
  import { dlog } from '../../debug.js';
7
7
  import { BaseAnimation } from './base.js';
8
- import { cancelFrame, clearTimer, requestFrame, setTimer } from './raf.js';
8
+ import { cancelFrame, clearTimer, requestFrame, setTimer, } from './raf.js';
9
9
  let cachedEaseInOut;
10
10
  function easeInOut() {
11
11
  if (cachedEaseInOut === undefined) {
@@ -80,7 +80,8 @@ export class TimingAnimation extends BaseAnimation {
80
80
  return;
81
81
  }
82
82
  this.onUpdate(this.fromValue +
83
- this.easing((now - this.startTime) / this.duration) * (this.toValue - this.fromValue));
83
+ this.easing((now - this.startTime) / this.duration) *
84
+ (this.toValue - this.fromValue));
84
85
  if (this.__active) {
85
86
  this.animationFrame = requestFrame(() => this.onFrame());
86
87
  }
@@ -40,7 +40,8 @@ function binarySubdivide(x, lower, upper, mX1, mX2) {
40
40
  else {
41
41
  lo = currentT;
42
42
  }
43
- } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS);
43
+ } while (Math.abs(currentX) > SUBDIVISION_PRECISION &&
44
+ ++i < SUBDIVISION_MAX_ITERATIONS);
44
45
  return currentT;
45
46
  }
46
47
  function newtonRaphsonIterate(x, guess, mX1, mX2) {
@@ -1,12 +1,8 @@
1
1
  import { AnimatedWithChildren } from './graph';
2
2
  import { AnimatedValue } from './value';
3
3
  import type { INativeNodeConfig, IPlatformConfig } from './native/native-animated';
4
- export interface IRgbaValue {
5
- r: number;
6
- g: number;
7
- b: number;
8
- a: number;
9
- }
4
+ import { normalizeColor, type IRgbaValue } from './rgba';
5
+ export { normalizeColor, type IRgbaValue };
10
6
  type IChannel = number | AnimatedValue;
11
7
  interface IRgbaInput {
12
8
  r: IChannel;
@@ -15,7 +11,6 @@ interface IRgbaInput {
15
11
  a: IChannel;
16
12
  }
17
13
  export type IColorInput = IRgbaInput | IRgbaValue | string | number;
18
- export declare function normalizeColor(color: string | number): IRgbaValue | undefined;
19
14
  export declare class AnimatedColor extends AnimatedWithChildren {
20
15
  readonly r: AnimatedValue;
21
16
  readonly g: AnimatedValue;
@@ -34,4 +29,3 @@ export declare class AnimatedColor extends AnimatedWithChildren {
34
29
  __makeNative(platformConfig?: IPlatformConfig): void;
35
30
  __getNativeConfig(): INativeNodeConfig;
36
31
  }
37
- export {};
@@ -6,57 +6,16 @@
6
6
  // normalizeColor / processColorObject, which live outside shared).
7
7
  import { AnimatedWithChildren, flushValue } from './graph.js';
8
8
  import { AnimatedValue } from './value.js';
9
- const DEFAULT_COLOR = { r: 0, g: 0, b: 0, a: 1 };
9
+ import { DEFAULT_COLOR, normalizeColor } from './rgba.js';
10
+ // Re-exported so every existing `from './color'` import keeps working; the definitions now
11
+ // live in the dependency-free ./rgba leaf (see its header for the cycle it breaks).
12
+ export { normalizeColor };
10
13
  function isRgbaInput(value) {
11
- return typeof value === 'object' && value !== null && 'r' in value && 'g' in value;
14
+ return (typeof value === 'object' && value !== null && 'r' in value && 'g' in value);
12
15
  }
13
16
  function toChannel(value) {
14
17
  return value instanceof AnimatedValue ? value : new AnimatedValue(value);
15
18
  }
16
- // Decompose a #hex (3/4/6/8), rgb()/rgba(), or 0xRRGGBBAA number into channels.
17
- // undefined when unparseable (a named/platform color), so the caller falls back to
18
- // the default rather than throwing inside a render. Exported so interpolation's
19
- // color path parses through the same RGBA decoder (DRY) rather than duplicating it.
20
- export function normalizeColor(color) {
21
- if (typeof color === 'number') {
22
- const c = color >>> 0;
23
- return { r: (c >>> 24) & 255, g: (c >>> 16) & 255, b: (c >>> 8) & 255, a: (c & 255) / 255 };
24
- }
25
- const trimmed = color.trim();
26
- if (trimmed.startsWith('#'))
27
- return parseHex(trimmed);
28
- if (/^rgba?\(/i.test(trimmed))
29
- return parseRgb(trimmed);
30
- return undefined;
31
- }
32
- function parseHex(hex) {
33
- let body = hex.slice(1);
34
- if (body.length === 3 || body.length === 4) {
35
- body = body
36
- .split('')
37
- .map(c => c + c)
38
- .join('');
39
- }
40
- if (body.length !== 6 && body.length !== 8)
41
- return undefined;
42
- const int = Number.parseInt(body, 16);
43
- if (Number.isNaN(int))
44
- return undefined;
45
- if (body.length === 6) {
46
- return { r: (int >>> 16) & 255, g: (int >>> 8) & 255, b: int & 255, a: 1 };
47
- }
48
- const c = int >>> 0;
49
- return { r: (c >>> 24) & 255, g: (c >>> 16) & 255, b: (c >>> 8) & 255, a: (c & 255) / 255 };
50
- }
51
- function parseRgb(str) {
52
- const match = /^rgba?\(([^)]+)\)$/i.exec(str);
53
- if (match === null)
54
- return undefined;
55
- const parts = match[1].split(',').map(part => Number.parseFloat(part.trim()));
56
- if (parts.length < 3 || parts.some(n => Number.isNaN(n)))
57
- return undefined;
58
- return { r: parts[0], g: parts[1], b: parts[2], a: parts.length >= 4 ? parts[3] : 1 };
59
- }
60
19
  // Resolve any input form to four concrete channel values (numbers or pre-built
61
20
  // AnimatedValues), so the constructor can wrap each in an AnimatedValue.
62
21
  function resolveInput(value) {
@@ -100,7 +59,9 @@ export class AnimatedColor extends AnimatedWithChildren {
100
59
  // pattern). flushValue dedupes by leaf identity, so a single flush rebuilds
101
60
  // every bound prop once even though four channels changed.
102
61
  setValue(value) {
103
- const rgba = typeof value === 'object' ? value : (normalizeColor(value) ?? DEFAULT_COLOR);
62
+ const rgba = typeof value === 'object'
63
+ ? value
64
+ : (normalizeColor(value) ?? DEFAULT_COLOR);
104
65
  this.withSuspendedCallbacks(() => {
105
66
  this.r.setValue(rgba.r);
106
67
  this.g.setValue(rgba.g);
@@ -4,15 +4,12 @@
4
4
  import { bezier } from './bezier.js';
5
5
  let cachedEase;
6
6
  export const Easing = {
7
- // A stepping function: 1 for any positive n, else 0.
8
7
  step0(n) {
9
8
  return n > 0 ? 1 : 0;
10
9
  },
11
- // A stepping function: 1 once n >= 1, else 0.
12
10
  step1(n) {
13
11
  return n >= 1 ? 1 : 0;
14
12
  },
15
- // Linear, f(t) = t.
16
13
  linear(t) {
17
14
  return t;
18
15
  },
@@ -23,11 +20,9 @@ export const Easing = {
23
20
  }
24
21
  return cachedEase(t);
25
22
  },
26
- // Quadratic, f(t) = t * t.
27
23
  quad(t) {
28
24
  return t * t;
29
25
  },
30
- // Cubic, f(t) = t * t * t.
31
26
  cubic(t) {
32
27
  return t * t * t;
33
28
  },
@@ -35,15 +30,12 @@ export const Easing = {
35
30
  poly(n) {
36
31
  return t => Math.pow(t, n);
37
32
  },
38
- // Sinusoidal.
39
33
  sin(t) {
40
34
  return 1 - Math.cos((t * Math.PI) / 2);
41
35
  },
42
- // Circular.
43
36
  circle(t) {
44
37
  return 1 - Math.sqrt(1 - t * t);
45
38
  },
46
- // Exponential.
47
39
  exp(t) {
48
40
  return Math.pow(2, 10 * (t - 1));
49
41
  },
@@ -17,14 +17,16 @@ import { dlog } from '../debug.js';
17
17
  import { getNativeTag, whenCommitted } from '../commit.js';
18
18
  import { isSymbioteNode } from '../node.js';
19
19
  import { AnimatedNode, flushValue } from './graph.js';
20
- import { isNativeAnimatedAvailable, nativeAnimated } from './native/native-animated.js';
20
+ import { isNativeAnimatedAvailable, nativeAnimated, } from './native/native-animated.js';
21
21
  import { isRecord } from '../type-guards.js';
22
22
  // A value node settable from an event field. AnimatedValue carries setValue; we
23
23
  // duck-type it (rather than importing AnimatedValue) to keep this leaf-agnostic
24
24
  // and avoid a value<->event import cycle.
25
25
  function settableValue(node) {
26
26
  const candidate = Reflect.get(node, 'setValue');
27
- return typeof candidate === 'function' ? value => candidate.call(node, value) : undefined;
27
+ return typeof candidate === 'function'
28
+ ? value => candidate.call(node, value)
29
+ : undefined;
28
30
  }
29
31
  // Walk the mapping to every leaf AnimatedNode, recording its key path. Shared by
30
32
  // the JS handler (to set values) and the native attach (to register paths).
@@ -128,7 +130,9 @@ export class AnimatedEvent {
128
130
  if (!this.attachedNatively) {
129
131
  // Paths are stored relative to `nativeEvent` (the native module excludes
130
132
  // that prefix); the JS event arg still carries it, so re-add it here.
131
- const nativeEvent = isRecord(args[0]) ? Reflect.get(args[0], 'nativeEvent') : undefined;
133
+ const nativeEvent = isRecord(args[0])
134
+ ? Reflect.get(args[0], 'nativeEvent')
135
+ : undefined;
132
136
  for (const mapped of this.mappedValues) {
133
137
  const extracted = extractAtPath(nativeEvent, mapped.path);
134
138
  if (extracted === undefined)
@@ -1,11 +1,8 @@
1
1
  import { type INativeNodeConfig, type IPlatformConfig } from './native/native-animated';
2
- import type { IInterpolationConfig } from './interpolation';
3
- import type { AnimatedInterpolation } from './interpolation-node';
2
+ import { type IInterpolationConfig } from './interpolation';
4
3
  export type IValueListener = (state: {
5
4
  value: number | string;
6
5
  }) => void;
7
- type IInterpolationFactory = (parent: AnimatedNode, config: IInterpolationConfig) => AnimatedInterpolation;
8
- export declare function registerInterpolationFactory(factory: IInterpolationFactory): void;
9
6
  export declare class AnimatedNode {
10
7
  private readonly listeners;
11
8
  private suspendCallbacks;
@@ -41,4 +38,15 @@ export declare class AnimatedWithChildren extends AnimatedNode {
41
38
  __callListeners(value: number | string): void;
42
39
  }
43
40
  export declare function flushValue(rootNode: AnimatedNode): void;
44
- export {};
41
+ export declare class AnimatedInterpolation extends AnimatedWithChildren {
42
+ private readonly parent;
43
+ private readonly config;
44
+ private interpolation;
45
+ constructor(parent: AnimatedNode, config: IInterpolationConfig);
46
+ private getInterpolation;
47
+ __getValue(): number | string;
48
+ __attach(): void;
49
+ __detach(): void;
50
+ __makeNative(platformConfig?: IPlatformConfig): void;
51
+ __getNativeConfig(): INativeNodeConfig;
52
+ }
@@ -10,11 +10,9 @@
10
10
  // B) bottom-up: each flagged leaf re-pulls its whole subtree via __getValue()
11
11
  // to rebuild composite props (e.g. transform from many parents).
12
12
  import { generateNativeNodeTag, nativeAnimated, } from './native/native-animated.js';
13
+ import { checkValidRanges, createInterpolation, } from './interpolation.js';
14
+ import { dlog } from '../debug.js';
13
15
  let nextListenerId = 1;
14
- let createInterpolationNode;
15
- export function registerInterpolationFactory(factory) {
16
- createInterpolationNode = factory;
17
- }
18
16
  // Depth of the currently-active withSuspendedCallbacks blocks. While > 0, a
19
17
  // composite setter (AnimatedColor.setValue) is driving several channels in a row;
20
18
  // each channel's own flushValue is suppressed so the bound leaf commits ONCE (at
@@ -83,7 +81,10 @@ export class AnimatedNode {
83
81
  // (AnimatedNode.js:146-148) rather than in each node's config method.
84
82
  const config = this.platformConfig === undefined
85
83
  ? this.__getNativeConfig()
86
- : { ...this.__getNativeConfig(), platformConfig: this.platformConfig };
84
+ : {
85
+ ...this.__getNativeConfig(),
86
+ platformConfig: this.platformConfig,
87
+ };
87
88
  nativeAnimated.createAnimatedNode(this.nativeTag, config);
88
89
  }
89
90
  return this.nativeTag;
@@ -104,13 +105,10 @@ export class AnimatedNode {
104
105
  }
105
106
  // Map this node's value before it reaches a prop, e.g. 0..1 -> 0..10. Defined once
106
107
  // here (Information Expert: every node subclass wants the identical one-liner)
107
- // instead of duplicated per subclass; see registerInterpolationFactory above for
108
- // why the concrete constructor is injected rather than imported directly.
108
+ // instead of duplicated per subclass. AnimatedInterpolation lives at the bottom of
109
+ // THIS file rather than its own module on purpose — see the comment there.
109
110
  interpolate(config) {
110
- if (createInterpolationNode === undefined) {
111
- throw new Error('AnimatedNode.interpolate: interpolation factory not registered');
112
- }
113
- return createInterpolationNode(this, config);
111
+ return new AnimatedInterpolation(this, config);
114
112
  }
115
113
  __addChild(_child) { }
116
114
  __removeChild(_child) { }
@@ -199,8 +197,18 @@ export class AnimatedWithChildren extends AnimatedNode {
199
197
  super.__callListeners(value);
200
198
  // A native-driven node's children are updated natively; don't also walk them
201
199
  // here (their values aren't tracked in JS while native owns the animation).
202
- if (this.isNative)
200
+ //
201
+ // Diagnostic seam, permanent: this early return is what silences a JS listener sitting on a
202
+ // CHILD of a value that was made native (a sticky header's interpolation under a natively
203
+ // attached scroll value). A consumer whose only driver is that listener then goes dead with no
204
+ // other symptom - see the sticky-header bootstrap notes. Logged only when children are
205
+ // actually being skipped, so a childless native value stays quiet.
206
+ if (this.isNative) {
207
+ if (this.children.length > 0) {
208
+ dlog(`STICKY[cascade] native value silences ${this.children.length} child JS listener(s) value=${String(value)}`);
209
+ }
203
210
  return;
211
+ }
204
212
  for (const child of this.children) {
205
213
  child.__callListeners(numericValueOf(child));
206
214
  }
@@ -216,7 +224,9 @@ function numericValueOf(node) {
216
224
  // under useDefineForClassFields. So leaves are detected structurally instead.
217
225
  function leafUpdate(node) {
218
226
  const candidate = Reflect.get(node, 'update');
219
- return typeof candidate === 'function' ? () => candidate.call(node) : undefined;
227
+ return typeof candidate === 'function'
228
+ ? () => candidate.call(node)
229
+ : undefined;
220
230
  }
221
231
  // Top-down walk to the leaves, then re-pull each leaf (deduped by node identity,
222
232
  // so a diamond in the graph still updates a leaf once). Suppressed inside a
@@ -238,3 +248,72 @@ export function flushValue(rootNode) {
238
248
  collect(rootNode);
239
249
  leaves.forEach(update => update());
240
250
  }
251
+ // AnimatedInterpolation: a graph node that maps its parent's numeric value through an
252
+ // interpolation. Ported from RN's AnimatedInterpolation.js (numeric, string-with-units and
253
+ // color output ranges; platform colors stay out of scope, color.ts defers them).
254
+ //
255
+ // IT LIVES HERE, NOT IN ITS OWN MODULE, AND MUST STAY HERE. It used to sit in
256
+ // `interpolation-node.ts` and hand its constructor back to `AnimatedNode.interpolate()` through
257
+ // a `registerInterpolationFactory` call executed at module load — the usual trick for breaking
258
+ // an import cycle (this module owns the base classes that module extended). That trick is
259
+ // UNSOUND under Metro's production bundle: `inlineRequires` (production-only) defers a module's
260
+ // `require()` to the first use of its binding, nothing in the codebase ever names
261
+ // `AnimatedInterpolation` as a value, and a barrel's `export { X } from './x'` compiles to a
262
+ // lazy getter — so the module never evaluated, the factory was never registered, and the first
263
+ // `.interpolate()` on a device threw `interpolation factory not registered`, blanking the screen
264
+ // in RELEASE builds only (dev was fine, every headless test was fine, and the code was even
265
+ // present in the bundle — it just never ran). Adding a bare `import './interpolation-node'` to
266
+ // the barrel does NOT fix it either: Babel merges that import with the adjacent re-export of the
267
+ // same specifier, and the merged dependency stays lazy. Both were tried on device 2026-08-14.
268
+ //
269
+ // Keeping the class next to its base class removes the cycle, the factory, and the entire class
270
+ // of load-order bug with it: `interpolate()` constructs it directly and there is nothing left to
271
+ // register. Do not "tidy" it back into its own file.
272
+ export class AnimatedInterpolation extends AnimatedWithChildren {
273
+ parent;
274
+ config;
275
+ interpolation;
276
+ constructor(parent, config) {
277
+ super();
278
+ this.parent = parent;
279
+ this.config = config;
280
+ // Validate eagerly so a bad range fails at construction, not first frame.
281
+ checkValidRanges(config.inputRange, config.outputRange);
282
+ }
283
+ getInterpolation() {
284
+ if (this.interpolation === undefined) {
285
+ this.interpolation = createInterpolation(this.config);
286
+ }
287
+ return this.interpolation;
288
+ }
289
+ __getValue() {
290
+ const parentValue = this.parent.__getValue();
291
+ if (typeof parentValue !== 'number') {
292
+ throw new Error('Cannot interpolate an input which is not a number');
293
+ }
294
+ return this.getInterpolation()(parentValue);
295
+ }
296
+ __attach() {
297
+ this.parent.__addChild(this);
298
+ super.__attach();
299
+ }
300
+ __detach() {
301
+ this.parent.__removeChild(this);
302
+ super.__detach();
303
+ }
304
+ // Make the upstream value native first, so the parent->interpolation edge can be
305
+ // wired when this node is reached from a leaf rather than from the value.
306
+ __makeNative(platformConfig) {
307
+ this.parent.__makeNative(platformConfig);
308
+ super.__makeNative(platformConfig);
309
+ }
310
+ __getNativeConfig() {
311
+ return {
312
+ type: 'interpolation',
313
+ inputRange: this.config.inputRange,
314
+ outputRange: this.config.outputRange,
315
+ extrapolateLeft: this.config.extrapolateLeft ?? this.config.extrapolate ?? 'extend',
316
+ extrapolateRight: this.config.extrapolateRight ?? this.config.extrapolate ?? 'extend',
317
+ };
318
+ }
319
+ }
@@ -1,7 +1,6 @@
1
- export { AnimatedNode, AnimatedWithChildren, flushValue, type IValueListener } from './graph';
1
+ export { AnimatedNode, AnimatedWithChildren, AnimatedInterpolation, flushValue, type IValueListener, } from './graph';
2
2
  export { AnimatedValue } from './value';
3
3
  export { AnimatedValueXY, type IValueXY } from './value-xy';
4
- export { AnimatedInterpolation } from './interpolation-node';
5
4
  export { AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, add, subtract, multiply, divide, modulo, diffClamp, } from './operators';
6
5
  export { AnimatedEvent, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, type IEventConfig, type IEventListener, type IAnimatedEventHandler, type INativeEventAttachment, } from './event';
7
6
  export { AnimatedColor, type IRgbaValue, type IColorInput } from './color';
@@ -15,6 +14,7 @@ export { AnimatedTracking } from './animations/tracking';
15
14
  export { timing, spring, decay, parallel, sequence, stagger, loop, delay, type ICompositeAnimation, type ITimingConfig, type ISpringConfig, type IDecayConfig, type IParallelConfig, type ILoopAnimationConfig, } from './animations/composition';
16
15
  export { nativeAnimated, isNativeAnimatedAvailable, type INativeNodeConfig, type INativeAnimationConfig, type INativeEventMapping, type IPlatformConfig, } from './native/native-animated';
17
16
  export { AnimatedProps } from './props';
17
+ export { createAnimatedLeafLifecycle, type IAnimatedLeafLifecycle, } from './leaf-lifecycle';
18
18
  export { AnimatedStyle, AnimatedTransform } from './style';
19
19
  export { AnimatedMock } from './mock';
20
- export { reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode } from './shared';
20
+ export { reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode, } from './shared';
@@ -1,10 +1,9 @@
1
1
  // @symbiote-native/engine/animated: the framework-agnostic, JS-driven Animated engine.
2
2
  // The value graph, easing, interpolation and drivers are pure JS with no React
3
3
  // and no native dependency; every adapter re-exports them.
4
- export { AnimatedNode, AnimatedWithChildren, flushValue } from './graph.js';
4
+ export { AnimatedNode, AnimatedWithChildren, AnimatedInterpolation, flushValue, } from './graph.js';
5
5
  export { AnimatedValue } from './value.js';
6
6
  export { AnimatedValueXY } from './value-xy.js';
7
- export { AnimatedInterpolation } from './interpolation-node.js';
8
7
  export { AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, add, subtract, multiply, divide, modulo, diffClamp, } from './operators.js';
9
8
  export { AnimatedEvent, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, } from './event.js';
10
9
  export { AnimatedColor } from './color.js';
@@ -22,7 +21,8 @@ export { nativeAnimated, isNativeAnimatedAvailable, } from './native/native-anim
22
21
  // no React/Vue). They live here with the rest of the graph; every adapter's
23
22
  // createAnimatedComponent + Animated namespace re-exports them.
24
23
  export { AnimatedProps } from './props.js';
24
+ export { createAnimatedLeafLifecycle, } from './leaf-lifecycle.js';
25
25
  export { AnimatedStyle, AnimatedTransform } from './style.js';
26
26
  export { AnimatedMock } from './mock.js';
27
27
  // Framework-agnostic createAnimatedComponent helpers. Both adapters import them.
28
- export { reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode } from './shared.js';
28
+ export { reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode, } from './shared.js';
@@ -4,7 +4,7 @@
4
4
  // extrapolation. A numeric output range stays number -> number; a string output
5
5
  // range (values-with-units like '0deg' or colors like '#000') is interpolated
6
6
  // per numeric token and reassembled. See createStringInterpolation below.
7
- import { normalizeColor } from './color.js';
7
+ import { normalizeColor } from './rgba.js';
8
8
  import { Easing } from './easing.js';
9
9
  function interpolateSegment(input, inputMin, inputMax, outputMin, outputMax, easing, extrapolateLeft, extrapolateRight) {
10
10
  let result = input;
@@ -171,7 +171,9 @@ function createStringInterpolation(config, outputRange) {
171
171
  return input => {
172
172
  const values = interpolations.map(interpolation => interpolation(input));
173
173
  let i = 0;
174
- return template.map(c => (typeof c === 'number' ? values[i++] : c)).join('');
174
+ return template
175
+ .map(c => (typeof c === 'number' ? values[i++] : c))
176
+ .join('');
175
177
  };
176
178
  }
177
179
  // Colors: r,g,b must be integers, so round them; alpha stays continuous but is
@@ -195,5 +197,8 @@ export function createInterpolation(config) {
195
197
  if (isStringOutputRange(config.outputRange)) {
196
198
  return createStringInterpolation(config, config.outputRange);
197
199
  }
198
- return createNumericInterpolation({ ...config, outputRange: config.outputRange });
200
+ return createNumericInterpolation({
201
+ ...config,
202
+ outputRange: config.outputRange,
203
+ });
199
204
  }