@symbiote-native/react 2.0.0 → 3.0.1

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/README.md CHANGED
@@ -27,9 +27,9 @@ npm install @symbiote-native/react react-native react
27
27
  ```
28
28
 
29
29
  `react-native` and `react` stay your app's own top-level dependencies — SymbioteNative doesn't hide
30
- them, it only replaces the JS renderer that drives them. There's no `create-symbiote` scaffolder
31
- yet, so the Metro config and the `index.js` entry seam below come from
32
- [`examples/react`](../../examples/react) rather than a generator.
30
+ them, it only replaces the JS renderer that drives them. `npx @symbiote-native/cli new --framework react`
31
+ scaffolds the Metro config and the `index.js` entry seam below for a new app; wiring them into an
32
+ existing one still follows [`examples/react`](../../examples/react) rather than a generator.
33
33
 
34
34
  ---
35
35
 
@@ -80,7 +80,7 @@ export function FlatList(props) {
80
80
  getItem: (_source, index) => rows[index],
81
81
  getItemCount: () => rows.length,
82
82
  renderItem: renderRow,
83
- keyExtractor: rowKeyExtractor,
83
+ keyExtractor: (row) => rowKeyExtractor(row, keyExtractor),
84
84
  onViewableItemsChanged: rowOnViewableItemsChanged,
85
85
  viewabilityConfigCallbackPairs: rowViewabilityPairs,
86
86
  ItemSeparatorComponent: rowSeparatorComponent,
@@ -10,7 +10,7 @@
10
10
  // around its children.
11
11
  import { createElement, useEffect, useRef, useState, } from 'react';
12
12
  import { dlog, Keyboard, Platform, } from '@symbiote-native/engine';
13
- import { computeInset, keyboardAvoidingEventNamesFor, readKeyboardFrame, readLayoutFrame, readPrefersCrossFadeTransitions, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
13
+ import { computeInset, configureKeyboardAvoidingAnimation, keyboardAvoidingEventNamesFor, readKeyboardAnimationTiming, readKeyboardFrame, readLayoutFrame, readPrefersCrossFadeTransitions, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
14
14
  import {} from '../../components.js';
15
15
  // The two notifications to subscribe to, per host (iOS takes the `will` pair so the view rides
16
16
  // up with the keyboard animation; Android has no will-notifications). Resolved ONCE at module
@@ -47,6 +47,11 @@ export const KeyboardAvoidingView = props => {
47
47
  previousInset,
48
48
  prefersCrossFadeTransitions: prefersCrossFadeRef.current,
49
49
  });
50
+ // RN's `_updateBottomIfNecessary` skips the animation entirely when the inset did not
51
+ // change (`if (this._bottom === height) return;`) — arming a transition for a value that
52
+ // moves nowhere would only ever animate other props the app changed this frame.
53
+ if (next !== previousInset)
54
+ configureKeyboardAvoidingAnimation(readKeyboardAnimationTiming(payload), enabled);
50
55
  dlog(`KeyboardAvoidingView show -> inset ${next}`);
51
56
  return next;
52
57
  });
@@ -14,7 +14,9 @@ export interface IPressableProps extends IAccessibilityProps, IAriaProps {
14
14
  disabled?: boolean;
15
15
  focusable?: boolean;
16
16
  cancelable?: boolean;
17
+ blockNativeResponder?: boolean;
17
18
  hitSlop?: IRectOffset;
19
+ testOnly_pressed?: boolean;
18
20
  pressRetentionOffset?: IRectOffset;
19
21
  unstable_pressDelay?: number;
20
22
  minPressDuration?: number;
@@ -12,6 +12,8 @@ export interface ITouchableOpacityProps extends ITouchableBaseProps {
12
12
  export interface ITouchableHighlightProps extends ITouchableBaseProps {
13
13
  activeOpacity?: number;
14
14
  underlayColor?: string;
15
+ touchSoundDisabled?: boolean;
16
+ testOnly_pressed?: boolean;
15
17
  onShowUnderlay?: () => void;
16
18
  onHideUnderlay?: () => void;
17
19
  }
@@ -5,4 +5,5 @@ export type ITouchableNativeFeedbackProps = Omit<IPressableProps, 'style' | 'chi
5
5
  background?: INativeFeedbackBackground;
6
6
  useForeground?: boolean;
7
7
  children?: ReactNode;
8
+ touchSoundDisabled?: boolean;
8
9
  };
@@ -3,4 +3,5 @@ import type { IPressTimingProps } from '@symbiote-native/components';
3
3
  import type { IPressableProps } from '../pressable/pressable-props';
4
4
  export type ITouchableWithoutFeedbackProps = Omit<IPressableProps, 'style' | 'children'> & IPressTimingProps & {
5
5
  children?: ReactNode;
6
+ touchSoundDisabled?: boolean;
6
7
  };
@@ -38,7 +38,7 @@
38
38
  // nothing else about it.
39
39
  import { createElement, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, } from 'react';
40
40
  import { dlog, } from '@symbiote-native/engine';
41
- import { DEFAULT_END_REACHED_THRESHOLD, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_START_REACHED_THRESHOLD, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, buildListPlan, buildScrollViewHandle, buildViewabilityPairs, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readLayoutOffset, readScrollOffset, reduceList, resolveItemKey, } from '@symbiote-native/components';
41
+ import { DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, buildListPlan, buildScrollViewHandle, buildViewabilityPairs, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readLayoutOffset, readScrollOffset, reduceList, resolveItemKey, } from '@symbiote-native/components';
42
42
  function resolveElement(component) {
43
43
  if (component === undefined)
44
44
  return undefined;
@@ -65,7 +65,7 @@ function renderSeparatorElement(component, leadingItem, trailingItem, overrides)
65
65
  // generic). The ref is destructured here and wired through useImperativeHandle.
66
66
  export function VirtualizedList(props) {
67
67
  const forwardedRef = props.ref;
68
- const { data, getItem, getItemCount, renderItem, keyExtractor, getItemLayout, ItemSeparatorComponent, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, horizontal = false, inverted = false, extraData, onEndReached, onEndReachedThreshold = DEFAULT_END_REACHED_THRESHOLD, onStartReached, onStartReachedThreshold = DEFAULT_START_REACHED_THRESHOLD, onRefresh, refreshing, progressViewOffset, onViewableItemsChanged, viewabilityConfig, viewabilityConfigCallbackPairs, onScrollToIndexFailed, initialNumToRender = DEFAULT_INITIAL_NUM_TO_RENDER, initialScrollIndex, maxToRenderPerBatch = DEFAULT_MAX_TO_RENDER_PER_BATCH, updateCellsBatchingPeriod = DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, windowSize = DEFAULT_WINDOW_SIZE, stickyHeaderIndices, maintainVisibleContentPosition, style, contentContainerStyle,
68
+ const { data, getItem, getItemCount, renderItem, keyExtractor, getItemLayout, ItemSeparatorComponent, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, horizontal = false, inverted = false, extraData, onEndReached, onEndReachedThreshold, onStartReached, onStartReachedThreshold, onRefresh, refreshing, progressViewOffset, onViewableItemsChanged, viewabilityConfig, viewabilityConfigCallbackPairs, onScrollToIndexFailed, initialNumToRender = DEFAULT_INITIAL_NUM_TO_RENDER, initialScrollIndex, maxToRenderPerBatch = DEFAULT_MAX_TO_RENDER_PER_BATCH, updateCellsBatchingPeriod = DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, windowSize = DEFAULT_WINDOW_SIZE, stickyHeaderIndices, maintainVisibleContentPosition, style, contentContainerStyle,
69
69
  // Pulled out of the rest so the user's onScroll does NOT arrive raw via
70
70
  // ...accessibilityRest and overwrite the internal windowing handler. Composed below.
71
71
  onScroll: userOnScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd, scrollEventThrottle, keyboardShouldPersistTaps, keyboardDismissMode,
@@ -188,8 +188,12 @@ export function VirtualizedList(props) {
188
188
  const info = effect.info;
189
189
  const map = effect.map;
190
190
  const fire = () => {
191
- for (const pair of pairs)
192
- pair.onViewableItemsChanged(info);
191
+ for (const pair of pairs) {
192
+ pair.onViewableItemsChanged({
193
+ ...info,
194
+ viewabilityConfig: pair.viewabilityConfig,
195
+ });
196
+ }
193
197
  dispatchRef.current({ kind: 'viewable-fired', map });
194
198
  };
195
199
  if (viewableTimerRef.current !== null) {
@@ -16,10 +16,9 @@ import { descriptorFor } from '@symbiote-native/components';
16
16
  // A bare intrinsic tag has no wrapper to apply RN's per-primitive prop folds (id -> nativeID,
17
17
  // Text's ellipsizeMode / allowFontScaling defaults), so the renderer is the layer that must.
18
18
  // Shared with every other adapter, driven by the same HOST_PRIMITIVES spec.
19
- import { foldHostBag } from '@symbiote-native/components/fold-host-bag';
20
- // WHICH native view a primitive commits can depend on a prop (TextInput's `multiline`). A lowering
21
- // transform decides that from source text; a bare tag has no transform, so the choice is made here,
22
- // where the runtime value is known. Identity for every primitive that declares no alternative.
19
+ // WHICH native view a primitive commits can depend on a prop (TextInput's `multiline`), and that
20
+ // prop can be a runtime value — so the choice is made here, where it is known. Identity for every
21
+ // primitive that declares no alternative.
23
22
  import { resolveIntrinsicTag } from '@symbiote-native/components/resolve-intrinsic';
24
23
  // React-reserved prop keys that must never reach Fabric. `children` is the element
25
24
  // tree (handled via appendChild). `ref`/`key` are React 19 plain props: the reconciler
@@ -30,11 +29,24 @@ import { resolveIntrinsicTag } from '@symbiote-native/components/resolve-intrins
30
29
  function isReservedProp(key) {
31
30
  return key === 'children' || key === 'ref' || key === 'key';
32
31
  }
32
+ /**
33
+ * `Object.keys`, not `Object.entries`, and it is measured rather than stylistic.
34
+ *
35
+ * `entries` allocates the outer array AND a two-element array per key, eagerly, before the loop
36
+ * starts; `keys` allocates one array of strings and the lookup is a property read the engine is
37
+ * about to do anyway. This runs ONCE PER NODE CREATED — ten thousand times on a thousand-row create.
38
+ * Priced on `-O` Hermes by `object-iteration-cost.itest.ts` over a four-key bag: 0.42 us against
39
+ * 0.23, so **0.19 us per node, ~1.9 ms of a 102 ms create**.
40
+ *
41
+ * `for…in` is cheaper still (0.14) and is NOT used: it walks the prototype chain, and an inherited
42
+ * key would be routed as a prop. A reconciler's props object has no prototype today, which is a fact
43
+ * about today rather than a contract.
44
+ */
33
45
  function applyProps(node, props) {
34
- for (const [key, value] of Object.entries(props)) {
46
+ for (const key of Object.keys(props)) {
35
47
  if (isReservedProp(key))
36
48
  continue;
37
- routeProp(node, key, value);
49
+ routeProp(node, key, props[key]);
38
50
  }
39
51
  }
40
52
  function applyUpdate(node, oldProps, newProps) {
@@ -44,9 +56,12 @@ function applyUpdate(node, oldProps, newProps) {
44
56
  if (!Object.hasOwn(newProps, key))
45
57
  routeProp(node, key, undefined);
46
58
  }
47
- for (const [key, value] of Object.entries(newProps)) {
59
+ // Same `Object.keys` reason as `applyProps` above — this walk runs once per UPDATED node, which on
60
+ // a `partial` step is every row on the screen.
61
+ for (const key of Object.keys(newProps)) {
48
62
  if (isReservedProp(key))
49
63
  continue;
64
+ const value = newProps[key];
50
65
  if (value !== oldProps[key])
51
66
  routeProp(node, key, value);
52
67
  }
@@ -117,12 +132,8 @@ const reconciler = createReconciler({
117
132
  // other four adapters all pass it (vue `renderer/index.ts`, svelte `dom-shim/element.ts`,
118
133
  // solid `renderer.ts`, angular `renderer/index.ts`), and React is the one that did not.
119
134
  //
120
- // Inert on landing and deliberately so — nothing registers a behavior under a tag React emits.
121
- // The `-managed` split is what keeps it inert once something does: React's TextInput and
122
- // Switch wrappers emit `text-input-managed` / `switch-managed`, so a machine
123
- // registered on the lowered tag cannot also attach to a wrapper-built node and run twice.
124
135
  const node = createElement(descriptor.component, descriptor.isText, tag);
125
- applyProps(node, foldHostBag(type, props));
136
+ applyProps(node, props);
126
137
  return node;
127
138
  },
128
139
  createTextInstance(text, _container, hostContext) {
@@ -159,9 +170,7 @@ const reconciler = createReconciler({
159
170
  // an update. The node keeps its identity — an app's ref stays live — and the commit walk
160
171
  // re-creates the Fabric node because its viewName no longer matches the committed one.
161
172
  setNodeComponent(node, descriptorFor(resolveIntrinsicTag(type, newProps)).component);
162
- // BOTH sides folded, or the diff compares a raw `id` against a folded `nativeID` and writes
163
- // the alias twice while never clearing the raw key.
164
- applyUpdate(node, foldHostBag(type, oldProps), foldHostBag(type, newProps));
173
+ applyUpdate(node, oldProps, newProps);
165
174
  },
166
175
  commitTextUpdate(node, _oldText, newText) {
167
176
  setText(node, newText);
@@ -6,25 +6,7 @@ export declare const Animated: {
6
6
  Value: typeof AnimatedValue;
7
7
  ValueXY: typeof AnimatedValueXY;
8
8
  Color: typeof AnimatedColor;
9
- Easing: {
10
- step0(n: number): number;
11
- step1(n: number): number;
12
- linear(t: number): number;
13
- ease(t: number): number;
14
- quad(t: number): number;
15
- cubic(t: number): number;
16
- poly(n: number): import("@symbiote-native/engine").IEasingFunction;
17
- sin(t: number): number;
18
- circle(t: number): number;
19
- exp(t: number): number;
20
- elastic(bounciness?: number): import("@symbiote-native/engine").IEasingFunction;
21
- back(s?: number): import("@symbiote-native/engine").IEasingFunction;
22
- bounce(t: number): number;
23
- bezier(x1: number, y1: number, x2: number, y2: number): import("@symbiote-native/engine").IEasingFunction;
24
- in(easing: import("@symbiote-native/engine").IEasingFunction): import("@symbiote-native/engine").IEasingFunction;
25
- out(easing: import("@symbiote-native/engine").IEasingFunction): import("@symbiote-native/engine").IEasingFunction;
26
- inOut(easing: import("@symbiote-native/engine").IEasingFunction): import("@symbiote-native/engine").IEasingFunction;
27
- };
9
+ Easing: import("@symbiote-native/engine").IEasing;
28
10
  timing: typeof timing;
29
11
  spring: typeof spring;
30
12
  decay: typeof decay;
@@ -48,4 +30,32 @@ export declare const Animated: {
48
30
  FlatList: ReturnType<typeof createAnimatedComponent>;
49
31
  SectionList: ReturnType<typeof createAnimatedComponent>;
50
32
  createAnimatedComponent: typeof createAnimatedComponent;
33
+ } | {
34
+ Value: typeof AnimatedValue;
35
+ ValueXY: typeof AnimatedValueXY;
36
+ Color: typeof AnimatedColor;
37
+ Easing: import("@symbiote-native/engine").IEasing;
38
+ timing: (value: AnimatedValue, config: import("@symbiote-native/engine").ITimingConfig) => import("@symbiote-native/engine").ICompositeAnimation;
39
+ spring: (value: AnimatedValue, config: import("@symbiote-native/engine").ISpringConfig) => import("@symbiote-native/engine").ICompositeAnimation;
40
+ decay: (_value: AnimatedValue, _config: import("@symbiote-native/engine").IDecayConfig) => import("@symbiote-native/engine").ICompositeAnimation;
41
+ parallel: (animations: import("@symbiote-native/engine").ICompositeAnimation[], _config?: import("@symbiote-native/engine").IParallelConfig) => import("@symbiote-native/engine").ICompositeAnimation;
42
+ sequence: (animations: import("@symbiote-native/engine").ICompositeAnimation[]) => import("@symbiote-native/engine").ICompositeAnimation;
43
+ stagger: (_time: number, animations: import("@symbiote-native/engine").ICompositeAnimation[]) => import("@symbiote-native/engine").ICompositeAnimation;
44
+ loop: (_animation: import("@symbiote-native/engine").ICompositeAnimation) => import("@symbiote-native/engine").ICompositeAnimation;
45
+ delay: (_time: number) => import("@symbiote-native/engine").ICompositeAnimation;
46
+ add: typeof add;
47
+ subtract: typeof subtract;
48
+ multiply: typeof multiply;
49
+ divide: typeof divide;
50
+ modulo: typeof modulo;
51
+ diffClamp: typeof diffClamp;
52
+ event: typeof event;
53
+ forkEvent: typeof forkEvent;
54
+ unforkEvent: typeof unforkEvent;
55
+ View: import("react").ComponentType<import("./create-animated-component").IAnimatedComponentProps>;
56
+ Text: import("react").ComponentType<import("./create-animated-component").IAnimatedComponentProps>;
57
+ Image: import("react").ComponentType<import("./create-animated-component").IAnimatedComponentProps>;
58
+ FlatList: ReturnType<typeof createAnimatedComponent>;
59
+ SectionList: ReturnType<typeof createAnimatedComponent>;
60
+ createAnimatedComponent: typeof createAnimatedComponent;
51
61
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/react",
3
- "version": "2.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "SymbioteNative's React adapter — a react-reconciler host config (mutation mode) driving real native iOS/Android views through stock React Native Fabric.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -57,20 +57,20 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "react-reconciler": "^0.33.0",
60
- "@symbiote-native/components": "2.0.0",
60
+ "@symbiote-native/components": "3.0.1",
61
61
  "@symbiote-native/css-parser": "0.5.0"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "react": ">=19.0.0",
65
65
  "react-native": ">=0.86",
66
- "@symbiote-native/engine": "^0.5.0"
66
+ "@symbiote-native/engine": "^1.2.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/react": "^19.2.0",
70
70
  "@types/react-reconciler": "^0.32.2",
71
71
  "react": "19.2.3",
72
- "@symbiote-native/engine": "0.5.0",
73
- "@symbiote-native/test-utils": "0.3.0"
72
+ "@symbiote-native/engine": "1.2.0",
73
+ "@symbiote-native/test-utils": "0.4.1"
74
74
  },
75
75
  "scripts": {
76
76
  "typecheck": "tsc --build",