@sigx/lynx-runtime 0.4.4 → 0.4.6

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.
@@ -5,6 +5,16 @@ import type { SharedValue } from './shared-value.js';
5
5
  export type { BuiltinMapperName, MapperParams };
6
6
  /** Reset hook for tests. */
7
7
  export declare function resetAnimatedStyleBindingIds(): void;
8
+ /**
9
+ * Reactive binding spec — the `{ sv, mapperName, params }` triple an accessor
10
+ * returns to describe the binding that should currently be live, or `null`
11
+ * for "no binding right now".
12
+ */
13
+ export interface AnimatedStyleSpec {
14
+ sv: SharedValue<unknown>;
15
+ mapperName: BuiltinMapperName;
16
+ params?: MapperParams[BuiltinMapperName];
17
+ }
8
18
  /**
9
19
  * Bind a `MainThreadRef`'s element style to a `SharedValue` via a named
10
20
  * mapper. The MT-side runtime applies the mapper's output via
@@ -23,7 +33,24 @@ export declare function resetAnimatedStyleBindingIds(): void;
23
33
  * ALL of that element's bindings re-run so partial outputs don't drop the
24
34
  * unchanged-axis contribution.
25
35
  *
26
- * @example Ghost follower at 0.5×
36
+ * Two call shapes:
37
+ *
38
+ * - **Static** `useAnimatedStyle(ref, sv, mapperName, params)` — registers the
39
+ * binding once at setup and unregisters at unmount. Use this when the
40
+ * binding never changes for the element's lifetime (the common case:
41
+ * Draggable, Swipeable, drawer offsets, …).
42
+ *
43
+ * - **Reactive** `useAnimatedStyle(ref, () => spec | null)` — runs the
44
+ * accessor in an `effect` and (re)binds whenever the returned spec changes,
45
+ * or unbinds when it returns `null`. The binding always targets the *same*
46
+ * element, so the host element (and its subtree) stays mounted across
47
+ * rebinds. Use this when a single element transitions between animation
48
+ * states at runtime — e.g. a navigator layer that animates during a
49
+ * transition then settles to a static rest position. Re-binding on the
50
+ * same element is what lets `<Stack>` avoid remounting screens per
51
+ * animation phase (see `lynx-navigation`'s `<Layer>`).
52
+ *
53
+ * @example Ghost follower at 0.5× (static)
27
54
  * ```tsx
28
55
  * const tx = useSharedValue(0);
29
56
  * const ghostRef = useMainThreadRef<MainThread.Element | null>(null);
@@ -33,5 +60,16 @@ export declare function resetAnimatedStyleBindingIds(): void;
33
60
  * <Draggable translateX={tx} />
34
61
  * <view main-thread:ref={ghostRef} style={...} />
35
62
  * ```
63
+ *
64
+ * @example Animate-then-rest, no remount (reactive)
65
+ * ```tsx
66
+ * useAnimatedStyle(ref, () =>
67
+ * props.animation
68
+ * ? { sv: props.animation.progress, mapperName: props.animation.axis,
69
+ * params: { inputRange: [...], outputRange: [...] } }
70
+ * : null,
71
+ * );
72
+ * ```
36
73
  */
37
74
  export declare function useAnimatedStyle<N extends BuiltinMapperName>(elRef: MainThreadRef<MainThread.Element | null>, sv: SharedValue<unknown>, mapperName: N, params?: MapperParams[N]): void;
75
+ export declare function useAnimatedStyle(elRef: MainThreadRef<MainThread.Element | null>, spec: () => AnimatedStyleSpec | null): void;
@@ -1,4 +1,5 @@
1
1
  import { onUnmounted } from '@sigx/runtime-core';
2
+ import { effect } from '@sigx/reactivity';
2
3
  import { pushOp, scheduleFlush } from '../op-queue.js';
3
4
  import { OP } from '@sigx/lynx-runtime-internal';
4
5
  let nextBindingId = 1;
@@ -14,35 +15,31 @@ function allocBindingId() {
14
15
  return nextBindingId++;
15
16
  }
16
17
  /**
17
- * Bind a `MainThreadRef`'s element style to a `SharedValue` via a named
18
- * mapper. The MT-side runtime applies the mapper's output via
19
- * `setStyleProperties` on every flush boundary where the SharedValue's value changed.
20
- *
21
- * The mapper runs on the **Main Thread** with no thread crossing per frame —
22
- * the only inputs are the SharedValue's value (already on MT) and the `params` shipped
23
- * once in the registration op. Because mappers are looked up by *name*, the
24
- * SWC worklet transform doesn't have to capture a function reference (which
25
- * it can't), so this fits the existing worklet pipeline cleanly.
26
- *
27
- * Multiple bindings on the same element compose into a single
28
- * `setStyleProperties` call per flush. `transform` outputs concatenate in
29
- * registration order (e.g. `translateX(50px) translateY(20px)`); other style
30
- * keys merge by last-write-wins. When ANY binding on an element is dirty,
31
- * ALL of that element's bindings re-run so partial outputs don't drop the
32
- * unchanged-axis contribution.
33
- *
34
- * @example Ghost follower at 0.5×
35
- * ```tsx
36
- * const tx = useSharedValue(0);
37
- * const ghostRef = useMainThreadRef<MainThread.Element | null>(null);
38
- *
39
- * useAnimatedStyle(ghostRef, tx, 'translateX', { factor: 0.5 });
40
- *
41
- * <Draggable translateX={tx} />
42
- * <view main-thread:ref={ghostRef} style={...} />
43
- * ```
18
+ * Stable identity for a spec so the reactive binder can tell "same binding,
19
+ * different object instance" (no-op) from "actually changed" (rebind). The
20
+ * SharedValue's `_wvid` plus the mapper name + JSON-serialized params uniquely
21
+ * pin the binding the MT side would register.
44
22
  */
45
- export function useAnimatedStyle(elRef, sv, mapperName, params) {
23
+ function specSignature(cfg) {
24
+ if (!cfg)
25
+ return 'none';
26
+ let p = '';
27
+ try {
28
+ p = cfg.params == null ? '' : JSON.stringify(cfg.params);
29
+ }
30
+ catch {
31
+ p = '';
32
+ }
33
+ return `${cfg.mapperName}:${cfg.sv._wvid}:${p}`;
34
+ }
35
+ export function useAnimatedStyle(elRef, svOrSpec, mapperName, params) {
36
+ // Reactive form: drive register/unregister from an effect on the accessor.
37
+ if (typeof svOrSpec === 'function') {
38
+ bindReactiveAnimatedStyle(elRef, svOrSpec);
39
+ return;
40
+ }
41
+ // Static form (unchanged): register once, unregister at unmount.
42
+ const sv = svOrSpec;
46
43
  const bindingId = allocBindingId();
47
44
  pushOp(OP.REGISTER_AV_STYLE_BINDING, bindingId, elRef._wvid, sv._wvid, mapperName, params ?? null);
48
45
  scheduleFlush();
@@ -51,3 +48,51 @@ export function useAnimatedStyle(elRef, sv, mapperName, params) {
51
48
  scheduleFlush();
52
49
  });
53
50
  }
51
+ /**
52
+ * Reactive-binding implementation. Runs `accessor` in an `effect`; when the
53
+ * resulting spec's *signature* changes, unregisters the previous binding (if
54
+ * any) and registers the new one against the same element. A `null` spec means
55
+ * "no binding right now" — the element keeps the last style the MT applied
56
+ * (mappers are not reset on unregister), which for transition transforms is the
57
+ * resting identity transform.
58
+ *
59
+ * The signature dedupe is load-bearing: callers typically build a *fresh* spec
60
+ * object on every render (e.g. `computeLayers` in `<Stack>`), so the accessor's
61
+ * return value changes identity every render even when the logical binding is
62
+ * unchanged. Without the dedupe the binding would thrash register/unregister on
63
+ * every progress-driven re-render.
64
+ *
65
+ * Ordering note: we `scheduleFlush()` (microtask-deferred), never `flushNow()`,
66
+ * so the REGISTER op lands on the same channel and ordering the static form
67
+ * uses — preserving the "bindings register → SV resets inside the worklet →
68
+ * withTiming starts" sequence the navigator relies on.
69
+ */
70
+ function bindReactiveAnimatedStyle(elRef, accessor) {
71
+ let bindingId = null;
72
+ let sig = null;
73
+ const runner = effect(() => {
74
+ const cfg = accessor();
75
+ const nextSig = specSignature(cfg);
76
+ if (nextSig === sig)
77
+ return;
78
+ sig = nextSig;
79
+ if (bindingId !== null) {
80
+ pushOp(OP.UNREGISTER_AV_STYLE_BINDING, bindingId);
81
+ bindingId = null;
82
+ }
83
+ if (cfg) {
84
+ const id = allocBindingId();
85
+ bindingId = id;
86
+ pushOp(OP.REGISTER_AV_STYLE_BINDING, id, elRef._wvid, cfg.sv._wvid, cfg.mapperName, cfg.params ?? null);
87
+ }
88
+ scheduleFlush();
89
+ });
90
+ onUnmounted(() => {
91
+ runner.stop();
92
+ if (bindingId !== null) {
93
+ pushOp(OP.UNREGISTER_AV_STYLE_BINDING, bindingId);
94
+ bindingId = null;
95
+ scheduleFlush();
96
+ }
97
+ });
98
+ }
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export { registerBgSink, unregisterBgSink, ingestAvPublishes, resetBgAvBridge, b
18
18
  export { useSharedValue, SharedValue, } from './animated/shared-value.js';
19
19
  export type { SharedValueState } from './animated/shared-value.js';
20
20
  export { useAnimatedStyle, resetAnimatedStyleBindingIds, } from './animated/use-animated-style.js';
21
+ export type { AnimatedStyleSpec } from './animated/use-animated-style.js';
21
22
  export { useAnimatedValue, AnimatedValue, } from './animated/animated-value.js';
22
23
  export type { AnimatedValueState } from './animated/animated-value.js';
23
24
  export { runOnMainThread, runOnBackground, resetThreading, transformToWorklet, resetRunOnBackgroundState, } from './threading.js';
package/dist/jsx.d.ts CHANGED
@@ -233,6 +233,12 @@ export interface ListAttributes extends LynxCommonAttributes {
233
233
  }
234
234
  export interface ListItemAttributes extends LynxCommonAttributes {
235
235
  children?: any;
236
+ /**
237
+ * Unique, stable key identifying this item for the native recycler. Lynx
238
+ * uses it to diff and reuse `<list-item>` views as the list scrolls; it
239
+ * must be unique among siblings within the same `<list>`.
240
+ */
241
+ 'item-key'?: string;
236
242
  /** Item type for recycling (items with same item-type share a view pool) */
237
243
  'item-type'?: string | number;
238
244
  /** Sticky offset from top */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigx/lynx-runtime",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Lynx renderer for SignalX (background thread)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -41,7 +41,7 @@
41
41
  "dependencies": {
42
42
  "@sigx/runtime-core": "^0.4.8",
43
43
  "@sigx/reactivity": "^0.4.8",
44
- "@sigx/lynx-runtime-internal": "^0.4.4"
44
+ "@sigx/lynx-runtime-internal": "^0.4.6"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@lynx-js/types": "*"