@symbiote-native/react 0.2.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 (68) hide show
  1. package/build/bootstrap.js +4 -5
  2. package/build/components/activity-indicator/index.android.d.ts +2 -1
  3. package/build/components/activity-indicator/index.android.js +14 -10
  4. package/build/components/activity-indicator/index.ios.d.ts +2 -1
  5. package/build/components/activity-indicator/index.ios.js +9 -3
  6. package/build/components/activity-indicator/shared.d.ts +1 -2
  7. package/build/components/activity-indicator/shared.js +13 -11
  8. package/build/components/keyboard-avoiding-view/index.js +1 -1
  9. package/build/components/modal/index.d.ts +3 -2
  10. package/build/components/scroll-view/index.android.js +0 -2
  11. package/build/components/scroll-view/index.ios.js +10 -10
  12. package/build/components/scroll-view/shared.js +28 -32
  13. package/build/components/scroll-view/sticky-header.js +98 -74
  14. package/build/components/section-list/index.js +4 -6
  15. package/build/components/switch/index.android.d.ts +2 -1
  16. package/build/components/switch/index.android.js +9 -3
  17. package/build/components/switch/index.ios.d.ts +2 -1
  18. package/build/components/switch/index.ios.js +9 -3
  19. package/build/components/switch/shared.d.ts +2 -3
  20. package/build/components/switch/shared.js +44 -43
  21. package/build/components/text-input/index.js +1 -1
  22. package/build/components/touchable/index.js +22 -54
  23. package/build/components/virtualized-list/index.js +219 -305
  24. package/build/components/virtualized-section-list/index.js +2 -4
  25. package/build/create-tunnel/index.js +14 -20
  26. package/build/hooks/use-color-scheme.d.ts +1 -1
  27. package/build/hooks/use-color-scheme.js +1 -1
  28. package/build/hooks/use-window-dimensions.d.ts +1 -1
  29. package/build/hooks/use-window-dimensions.js +1 -1
  30. package/build/host-instance.js +5 -6
  31. package/build/index.d.ts +6 -31
  32. package/build/index.js +15 -25
  33. package/build/modules/animated/create-animated-component.js +24 -65
  34. package/build/render.js +28 -2
  35. package/metro-css-parser.cjs +7 -12
  36. package/package.json +4 -4
  37. package/build/modules/action-sheet-ios.d.ts +0 -2
  38. package/build/modules/action-sheet-ios.js +0 -4
  39. package/build/modules/alert.d.ts +0 -2
  40. package/build/modules/alert.js +0 -5
  41. package/build/modules/app-state.d.ts +0 -2
  42. package/build/modules/app-state.js +0 -3
  43. package/build/modules/appearance.d.ts +0 -2
  44. package/build/modules/appearance.js +0 -3
  45. package/build/modules/back-handler.d.ts +0 -2
  46. package/build/modules/back-handler.js +0 -3
  47. package/build/modules/dimensions.d.ts +0 -2
  48. package/build/modules/dimensions.js +0 -3
  49. package/build/modules/i18n-manager.d.ts +0 -2
  50. package/build/modules/i18n-manager.js +0 -4
  51. package/build/modules/keyboard.d.ts +0 -2
  52. package/build/modules/keyboard.js +0 -3
  53. package/build/modules/layout-animation.d.ts +0 -2
  54. package/build/modules/layout-animation.js +0 -3
  55. package/build/modules/linking.d.ts +0 -2
  56. package/build/modules/linking.js +0 -4
  57. package/build/modules/permissions-android.d.ts +0 -2
  58. package/build/modules/permissions-android.js +0 -3
  59. package/build/modules/settings.d.ts +0 -1
  60. package/build/modules/settings.js +0 -4
  61. package/build/modules/share.d.ts +0 -2
  62. package/build/modules/share.js +0 -4
  63. package/build/modules/text-input-state.d.ts +0 -1
  64. package/build/modules/text-input-state.js +0 -3
  65. package/build/modules/toast-android.d.ts +0 -1
  66. package/build/modules/toast-android.js +0 -4
  67. package/build/modules/vibration.d.ts +0 -1
  68. package/build/modules/vibration.js +0 -4
@@ -1,26 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- // createTunnel — the cross-surface answer to createPortal's same-surface-only scope.
3
- // Prior art: pmndrs/tunnel-rat exists precisely because a real
4
- // createPortal cannot reach across two genuinely separate reconciler roots — see
5
- // github.com/facebook/react/issues/17147 ("react-reconciler & portals: missing root
6
- // instance"). The fix is the one the ecosystem already settled on: don't reach into a
7
- // foreign surface's commit machinery — let that surface commit ITSELF, normally, by reading
2
+ // createTunnel - the cross-surface answer to createPortal's same-surface-only scope. A real
3
+ // createPortal cannot reach across two separate reconciler roots (see
4
+ // github.com/facebook/react/issues/17147); the fix, same as pmndrs/tunnel-rat, is to not
5
+ // reach into a foreign surface's commit machinery - let that surface commit itself, reading
8
6
  // from a store it's already subscribed to.
9
7
  //
10
- // Both In and Out are COMPONENTS (the Vue twin's shape, and the same pattern as
11
- // Context.Provider/Context.Consumer) — NOT hooks. This isn't just nicer call-site ergonomics:
12
- // it's what makes the cascade in the first place structurally impossible. An earlier
13
- // hook-based version (`useTunnelIn`/`useTunnelOut`, called directly inside one component)
14
- // produced a genuine infinite render loop — a silent white screen on device, no thrown error
15
- // — because notify() forces a re-render of whichever component's useSyncExternalStore call
16
- // subscribed, and when THAT was the SAME component that also called useTunnelIn, its own
17
- // effect re-ran and notified again, forever (this custom renderer's synchronous commit loop
18
- // has no "Maximum update depth exceeded" guard to catch it). As separate components, notify()
19
- // only forces Out's OWN render scope — never In's, even when both are children of the same
20
- // parent — so the update has nowhere to bounce back to. `Out` lives inside whichever surface
21
- // should PAINT the content (e.g. a persistent overlay-host surface); `In` lives ANYWHERE, same
22
- // surface or a totally different one, and never touches a Fabric node directly, so there is
23
- // no "target must already be mounted" guard to satisfy at all.
8
+ // In and Out are COMPONENTS (Context.Provider/Consumer shape), never hooks - this is
9
+ // load-bearing, not just ergonomics. An earlier hook-based version caused a real infinite
10
+ // render loop on device (silent white screen, no thrown error): notify() re-renders
11
+ // whichever component's useSyncExternalStore subscription fired, and when that was the same
12
+ // component calling useTunnelIn, its own effect re-notified itself forever - this custom
13
+ // renderer's synchronous commit loop has no "Maximum update depth exceeded" guard. As
14
+ // separate components, notify() only forces Out's own render scope, so the update has
15
+ // nowhere to bounce back to. `Out` lives in whichever surface should PAINT the content; `In`
16
+ // lives anywhere - same surface or a different one - and never touches a Fabric node
17
+ // directly, so there's no "target must already be mounted" guard to satisfy.
24
18
  import { Fragment, useEffect, useId, useSyncExternalStore } from 'react';
25
19
  export function createTunnel() {
26
20
  const items = new Map();
@@ -1,2 +1,2 @@
1
- import { type IColorSchemeName } from '../modules/appearance';
1
+ import { type IColorSchemeName } from '@symbiote-native/engine';
2
2
  export declare function useColorScheme(): IColorSchemeName | null;
@@ -2,7 +2,7 @@
2
2
  // on change. Mirrors RN's Libraries/Utilities/useColorScheme.js: a thin
3
3
  // useSyncExternalStore over Appearance's change event + current scheme read.
4
4
  import { useSyncExternalStore } from 'react';
5
- import { Appearance } from '../modules/appearance.js';
5
+ import { Appearance } from '@symbiote-native/engine';
6
6
  const subscribe = (onStoreChange) => {
7
7
  const subscription = Appearance.addChangeListener(onStoreChange);
8
8
  return () => subscription.remove();
@@ -1,2 +1,2 @@
1
- import { type IDisplayMetrics } from '../modules/dimensions';
1
+ import { type IDisplayMetrics } from '@symbiote-native/engine';
2
2
  export declare function useWindowDimensions(): IDisplayMetrics;
@@ -4,7 +4,7 @@
4
4
  // subscribing to close the gap between the render-time get and the effect-time
5
5
  // listener. Only the window metrics changing triggers a re-render.
6
6
  import { useEffect, useState } from 'react';
7
- import { Dimensions } from '../modules/dimensions.js';
7
+ import { Dimensions } from '@symbiote-native/engine';
8
8
  export function useWindowDimensions() {
9
9
  const [dimensions, setDimensions] = useState(() => Dimensions.get('window'));
10
10
  useEffect(() => {
@@ -1,9 +1,8 @@
1
- // The public instance a host ref hands back and the ref -> tag lookup. toPublicInstance and
2
- // IHostInstance now live in @symbiote-native/engine (they depend only on engine internals, so every
3
- // adapter inherits the SAME public instance); this module re-exports them so the React surface
4
- // (`@symbiote-native/react` exports findNodeHandle + IHostInstance, host-config grafts via
5
- // toPublicInstance) is unchanged. findNodeHandle stays here: it is the React-shaped "ref ->
6
- // native tag" lookup over the engine's getNativeTag.
1
+ // The public instance a host ref hands back, and the ref -> tag lookup. toPublicInstance and
2
+ // IHostInstance live in @symbiote-native/engine (every adapter inherits the same public instance);
3
+ // this module re-exports them so the React surface (findNodeHandle + IHostInstance, host-config's
4
+ // toPublicInstance graft) is unchanged. findNodeHandle stays React-specific: it is the
5
+ // RN-shaped "ref -> native tag" lookup over the engine's getNativeTag.
7
6
  import { getNativeTag, isSymbioteNode, toPublicInstance, } from '@symbiote-native/engine';
8
7
  export { toPublicInstance };
9
8
  // The committed native tag of a host ref (or a bare tag, idempotent). RN's
package/build/index.d.ts CHANGED
@@ -12,8 +12,6 @@ export { TextInput } from './components/text-input';
12
12
  export type { ITextInputProps, ITextInputHandle } from './components/text-input';
13
13
  export { InputAccessoryView } from './components/input-accessory-view';
14
14
  export type { IInputAccessoryViewProps } from './components/input-accessory-view';
15
- export { Keyboard, KEYBOARD_EVENT } from './modules/keyboard';
16
- export type { IKeyboardEventName } from './modules/keyboard';
17
15
  export { KeyboardAvoidingView } from './components/keyboard-avoiding-view';
18
16
  export type { IKeyboardAvoidingViewProps, IKeyboardAvoidingBehavior, } from './components/keyboard-avoiding-view';
19
17
  export { StatusBar } from './modules/status-bar';
@@ -55,44 +53,21 @@ export { Animated, createAnimatedComponent } from './modules/animated';
55
53
  export { Platform, StyleSheet } from '@symbiote-native/engine';
56
54
  export { PlatformColor, DynamicColorIOS, processColor } from '@symbiote-native/engine';
57
55
  export type { IColorValue, IOpaqueColorValue, IDynamicColorIOSTuple, } from '@symbiote-native/engine';
58
- export { setNativeViewConfigSource } from '@symbiote-native/engine';
56
+ export { setNativeViewConfigSource, setColorProcessor, setDeviceEventSource, } from '@symbiote-native/engine';
57
+ export { dlog, isDebug } from '@symbiote-native/engine';
59
58
  export type { INativeViewConfig, INativeViewConfigSource } from '@symbiote-native/engine';
60
59
  export type { IPlatformStatic, IPlatformOSType, IPlatformConstantsIOS, IPlatformConstantsAndroid, IPlatformSelectSpec, } from '@symbiote-native/engine';
61
- export { Dimensions } from './modules/dimensions';
62
- export type { IDisplayMetrics, IDisplayMetricsAndroid, IDimensionsPayload, IDimensionsSet, IDimensionsKey, IDimensionsChangeListener, IDimensionsStatic, } from './modules/dimensions';
63
- export { PixelRatio } from '@symbiote-native/engine';
64
- export type { IPixelRatioStatic } from '@symbiote-native/engine';
60
+ export { Alert, Share, ActionSheetIOS, Linking, Vibration, ToastAndroid, Settings, I18nManager, Dimensions, Appearance, AppState, Keyboard, KEYBOARD_EVENT, BackHandler, PermissionsAndroid, PERMISSIONS, RESULTS, LayoutAnimation, PixelRatio, } from '@symbiote-native/engine';
61
+ export type { IAlertType, IAlertButtonStyle, IAlertButton, IAlertButtons, IAlertOptions, IShareContent, IShareOptions, IShareAction, IActionSheetIOSOptions, IShareActionSheetIOSOptions, IShareActionSheetError, IUrlEvent, II18nManagerConstants, IDisplayMetrics, IDisplayMetricsAndroid, IDimensionsPayload, IDimensionsSet, IDimensionsKey, IDimensionsChangeListener, IDimensionsStatic, IColorSchemeName, IColorSchemePreference, IAppStateStatus, IAppStateEvent, IKeyboardEventName, IKeyboardEvent, IKeyboardMetrics, IBackPressEventName, IBackPressHandler, IPermission, IPermissionStatus, IRationale, ILayoutAnimationType, ILayoutAnimationProperty, ILayoutAnimationConfig, ILayoutAnimationAnim, IPixelRatioStatic, } from '@symbiote-native/engine';
65
62
  export { useWindowDimensions } from './hooks/use-window-dimensions';
66
- export { Appearance } from './modules/appearance';
67
- export type { IColorSchemeName, IColorSchemePreference } from './modules/appearance';
68
63
  export { useColorScheme } from './hooks/use-color-scheme';
69
- export { AppState } from './modules/app-state';
70
- export type { IAppStateStatus, IAppStateEvent } from './modules/app-state';
71
- export { Alert } from './modules/alert';
72
- export type { IAlertType, IAlertButtonStyle, IAlertButton, IAlertButtons, IAlertOptions, } from './modules/alert';
73
- export { ActionSheetIOS } from './modules/action-sheet-ios';
74
- export type { IActionSheetIOSOptions, IShareActionSheetIOSOptions, IShareActionSheetError, } from './modules/action-sheet-ios';
75
- export { Linking } from './modules/linking';
76
- export type { IUrlEvent } from './modules/linking';
77
- export { Vibration } from './modules/vibration';
78
- export { Share } from './modules/share';
79
- export type { IShareContent, IShareOptions, IShareAction } from './modules/share';
80
64
  export { AccessibilityInfo } from './modules/accessibility-info';
81
65
  export type { IAccessibilityChangeEvent, IAccessibilityChangeEventName, IAccessibilityChangeEventHandler, IAccessibilityAnnouncementFinishedEvent, IAnnounceForAccessibilityOptions, IAccessibilityEventType, } from './modules/accessibility-info';
82
- export { I18nManager } from './modules/i18n-manager';
83
- export type { II18nManagerConstants } from './modules/i18n-manager';
84
- export { Settings } from './modules/settings';
85
66
  export { PanResponder } from '@symbiote-native/engine';
86
67
  export type { IPanResponderGestureState, IPanResponderCallbacks, IGestureResponderHandlers, IPanResponderInstance, } from '@symbiote-native/engine';
87
- export { LayoutAnimation } from './modules/layout-animation';
88
- export type { ILayoutAnimationType, ILayoutAnimationProperty, ILayoutAnimationConfig, ILayoutAnimationAnim, } from './modules/layout-animation';
89
68
  export { InteractionManager } from '@symbiote-native/engine';
90
69
  export type { IInteractionEvent, ISimpleTask, IPromiseTask, ITask, IHandle, } from '@symbiote-native/engine';
91
- export { BackHandler } from './modules/back-handler';
92
- export type { IBackPressEventName, IBackPressHandler } from './modules/back-handler';
93
- export { ToastAndroid } from './modules/toast-android';
94
- export { PermissionsAndroid, PERMISSIONS, RESULTS } from './modules/permissions-android';
95
- export type { IPermission, IPermissionStatus, IRationale } from './modules/permissions-android';
96
70
  export { TouchableNativeFeedback } from './components/touchable-native-feedback';
97
71
  export type { ITouchableNativeFeedbackProps, INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground, } from './components/touchable-native-feedback';
98
- export type { ISymbioteEvent } from '@symbiote-native/engine';
72
+ export type { ISymbioteEvent, ISymbioteNode, IRootTag } from '@symbiote-native/engine';
73
+ export type { ICellLayout, ISeparatorProps, ISeparators, IModalOrientationChangeEvent, IPressableAndroidRippleConfig, IEnterKeyHint, IInputMode, ISubmitBehavior, ITextInputSelection, IImageStatics, } from '@symbiote-native/components';
package/build/index.js CHANGED
@@ -7,7 +7,6 @@ export { ImageBackground } from './components/image-background/index.js';
7
7
  export { ScrollView } from './components/scroll-view';
8
8
  export { TextInput } from './components/text-input/index.js';
9
9
  export { InputAccessoryView } from './components/input-accessory-view/index.js';
10
- export { Keyboard, KEYBOARD_EVENT } from './modules/keyboard.js';
11
10
  export { KeyboardAvoidingView } from './components/keyboard-avoiding-view/index.js';
12
11
  export { StatusBar } from './modules/status-bar';
13
12
  export { Switch } from './components/switch';
@@ -51,37 +50,28 @@ export { Platform, StyleSheet } from '@symbiote-native/engine';
51
50
  // processColor runs a color through the injected platform processor. All pure /
52
51
  // seam-backed, so they live in shared and the adapter re-exports them.
53
52
  export { PlatformColor, DynamicColorIOS, processColor } from '@symbiote-native/engine';
54
- // Wired once by the app entry on a real host (like setColorProcessor): hands shared
55
- // RN's ViewConfig registry so third-party Fabric views auto-derive their metadata:
53
+ // The three app-entry seams, wired once on a real host, so the barrel exposes them together.
54
+ // setNativeViewConfigSource hands the engine RN's ViewConfig registry, which is how third-party
55
+ // Fabric views auto-derive their metadata:
56
56
  // setNativeViewConfigSource(name => ReactNativeViewConfigRegistry.get(name))
57
- export { setNativeViewConfigSource } from '@symbiote-native/engine';
58
- // Runtime modules: native-bridge consumers, same shape as Keyboard/StatusBar:
59
- // thin JS over getNativeModule + device events, no Fabric component of their own.
60
- export { Dimensions } from './modules/dimensions.js';
61
- export { PixelRatio } from '@symbiote-native/engine';
57
+ export { setNativeViewConfigSource, setColorProcessor, setDeviceEventSource, } from '@symbiote-native/engine';
58
+ // Diagnostics, gated by DEBUG (<keep_logs_gate_behind_DEBUG>): app code logs through the same
59
+ // seam the engine does instead of a bare console.log.
60
+ export { dlog, isDebug } from '@symbiote-native/engine';
61
+ // Imperative runtime modules: the SAME module every adapter shares, re-exported straight from
62
+ // @symbiote-native/engine so app code names only @symbiote-native/react (RN's single import
63
+ // root). Thin JS over getNativeModule + device events, no Fabric component of their own.
64
+ export { Alert, Share, ActionSheetIOS, Linking, Vibration, ToastAndroid, Settings, I18nManager, Dimensions, Appearance, AppState, Keyboard, KEYBOARD_EVENT, BackHandler, PermissionsAndroid, PERMISSIONS, RESULTS, LayoutAnimation, PixelRatio, } from '@symbiote-native/engine';
65
+ // React lifecycle over those core device-state modules.
62
66
  export { useWindowDimensions } from './hooks/use-window-dimensions.js';
63
- export { Appearance } from './modules/appearance.js';
64
67
  export { useColorScheme } from './hooks/use-color-scheme.js';
65
- export { AppState } from './modules/app-state.js';
66
- export { Alert } from './modules/alert.js';
67
- export { ActionSheetIOS } from './modules/action-sheet-ios.js';
68
- export { Linking } from './modules/linking.js';
69
- export { Vibration } from './modules/vibration.js';
70
- export { Share } from './modules/share.js';
71
68
  export { AccessibilityInfo } from './modules/accessibility-info';
72
- export { I18nManager } from './modules/i18n-manager.js';
73
- export { Settings } from './modules/settings.js';
74
69
  // Interaction subsystems: gestures, deferred work, and layout transitions.
75
70
  export { PanResponder } from '@symbiote-native/engine';
76
- export { LayoutAnimation } from './modules/layout-animation.js';
77
71
  // InteractionManager is pure JS, so it lives in shared; re-exported here so app code
78
72
  // names only @symbiote-native/react (RN's single import root).
79
73
  export { InteractionManager } from '@symbiote-native/engine';
80
- // Android-only surface (the second-platform pass). Each is a thin JS shim over an
81
- // Android native module / Fabric view, inert on iOS (no module → graceful no-op,
82
- // no native view → degrade to a plain container). Native module name correctness
83
- // can only be confirmed on a real device/simulator, not headless tests.
84
- export { BackHandler } from './modules/back-handler.js';
85
- export { ToastAndroid } from './modules/toast-android.js';
86
- export { PermissionsAndroid, PERMISSIONS, RESULTS } from './modules/permissions-android.js';
74
+ // Android-only surface (the second-platform pass): a thin JS shim over an Android Fabric view,
75
+ // inert on iOS (no native view -> degrades to a plain container). The Android-only MODULES
76
+ // (ToastAndroid, PermissionsAndroid, BackHandler) sit in the engine block above.
87
77
  export { TouchableNativeFeedback } from './components/touchable-native-feedback/index.js';
@@ -13,8 +13,8 @@
13
13
  // and detaches on unmount / when the leaf identity changes. The per-frame path is
14
14
  // then: value.setValue / animation -> flushValue -> AnimatedProps.update() ->
15
15
  // setNativeProps(node, partial).
16
- import { createElement, useEffect, useMemo, useRef, } from 'react';
17
- import { AnimatedProps, attachNativeEventHandler, isNativeAnimatedAvailable, reduceProps, readPassthroughStyle, resolveHostNode, } from '@symbiote-native/engine';
16
+ import { createElement, useEffect, useRef, } from 'react';
17
+ import { createAnimatedLeafLifecycle, isSymbioteNode, isNativeAnimatedAvailable, reduceProps, readPassthroughStyle, resolveHostNode, } from '@symbiote-native/engine';
18
18
  // A ref can be a function or a `.current` object; assign through both forms without
19
19
  // casting so a forwarded ref from the caller still receives the instance. Framework-
20
20
  // ref-specific, so it stays per-adapter (the rest of the wrap helpers are shared).
@@ -33,77 +33,36 @@ export function createAnimatedComponent(Component) {
33
33
  // Native driving is opt-in per the passthrough prop AND requires a real native module;
34
34
  // headless / unsupported hosts keep the JS flush path (and the existing JS smokes green).
35
35
  const wantsNative = passthrough != null && isNativeAnimatedAvailable();
36
- // One AnimatedProps leaf per distinct props object. Rebuilt when props change,
37
- // so a newly-added animated value joins the graph; the effect below swaps the
38
- // graph attachment to match.
39
- const animatedProps = useMemo(() => new AnimatedProps(rest), [rest]);
40
- // The leaf currently wired into the value graph. Tracked across renders so a swap
41
- // can attach the new leaf BEFORE detaching the old one.
42
- const attached = useRef(null);
43
- // The committed host node, captured by the ref below. Needed in the event-attach
44
- // effect (a native event binds to the node's tag, not the AnimatedProps leaf).
36
+ // The leaf lifecycle - build/swap/bind/detach, native event rebinding, and the
37
+ // rebuild-vs-skip decision - is the engine's, shared by every adapter
38
+ // (core/engine/src/animated/leaf-lifecycle.ts). React owns only WHEN to run it.
39
+ //
40
+ // This used to be `useMemo(() => new AnimatedProps(rest), [rest])`, which reads like the same
41
+ // guard but never was one: `rest` comes out of a rest-destructure, so it is a fresh object on
42
+ // every render and the memo's dependency always differed. The real content check now lives in
43
+ // the shared lifecycle.
44
+ const lifecycleRef = useRef(null);
45
+ lifecycleRef.current ??= createAnimatedLeafLifecycle('react');
46
+ const lifecycle = lifecycleRef.current;
47
+ // The committed host node, captured by the ref below - a native event binds to the node's
48
+ // tag, not the AnimatedProps leaf.
45
49
  const nodeRef = useRef(null);
46
- // Swap the graph attachment to the current leaf, attaching the new one FIRST and
47
- // detaching the previous one SECOND. Order is load-bearing: a shared Value node
48
- // self-detaches (and drops its native animation node) the instant its child count
49
- // hits zero, so detaching the old leaf before the new one is attached would kill a
50
- // running native-driven animation on any unrelated re-render. Detaching here (not
51
- // in a cleanup, which React runs BEFORE the next setup) keeps the new-before-old
52
- // order, mirroring RN's AnimatedComponent._attachProps.
50
+ // Reconcile after every commit. No dependency array on purpose: the props object is rebuilt
51
+ // by every render anyway, so a dependency list could only ever say "always" - the real
52
+ // rebuild-vs-skip decision is the lifecycle's, and it compares CONTENT by key identity.
53
53
  useEffect(() => {
54
- animatedProps.__attach();
55
- const previous = attached.current;
56
- attached.current = animatedProps;
57
- if (previous !== null && previous !== animatedProps) {
58
- previous.__detach();
59
- }
60
- }, [animatedProps]);
61
- // Native-driver trigger. Runs after attach: push the leaf -> style -> transform
62
- // -> interpolation -> value chain native so the props animate on the UI thread (no JS lag).
63
- // Cascades down to the source value; the scroll event attaches to that same value
64
- // (idempotent). __makeNative is idempotent, so re-firing on a leaf swap is safe.
54
+ lifecycle.reconcile(rest, isSymbioteNode(nodeRef.current) ? nodeRef.current : null, wantsNative);
55
+ });
56
+ // Final teardown: detach the last-attached leaf and any native event bindings on unmount.
65
57
  useEffect(() => {
66
- if (wantsNative)
67
- animatedProps.__makeNative();
68
- }, [animatedProps, wantsNative]);
69
- // Final teardown: detach the last-attached leaf when the component unmounts.
70
- useEffect(() => {
71
- return () => {
72
- if (attached.current !== null) {
73
- attached.current.__detach();
74
- attached.current = null;
75
- }
76
- };
77
- }, []);
78
- // Native-attach any Animated.event prop, e.g. onScroll={Animated.event(…,
79
- // {useNativeDriver:true})}, to the committed node, so the event drives its values on
80
- // the UI thread. attachNativeEventHandler no-ops (returns undefined) unless the prop is
81
- // a native event handler with a committed tag, so the JS path stays the fallback. The
82
- // __makeNative cascade then carries the bound interpolations/props native too. Keyed on
83
- // animatedProps (rebuilt with rest) so a new inline event re-attaches; cleanup detaches.
84
- useEffect(() => {
85
- const node = nodeRef.current;
86
- if (node === null)
87
- return;
88
- const detachers = [];
89
- for (const key of Object.keys(rest)) {
90
- const attachment = attachNativeEventHandler(node, key, rest[key]);
91
- if (attachment !== undefined)
92
- detachers.push(attachment.detach);
93
- }
94
- return () => {
95
- for (const detach of detachers)
96
- detach();
97
- };
98
- }, [animatedProps]);
58
+ return () => lifecycle.teardown();
59
+ }, [lifecycle]);
99
60
  // Callback ref: when the base component mounts, capture its public instance, resolve
100
61
  // it to the underlying host node (unwrapping a scroll-container handle), record THAT
101
62
  // for the event-attach effect and bind it to the leaf, but forward the ORIGINAL
102
63
  // instance to the caller, who expects the component's public handle (scrollTo, …).
103
64
  const captureRef = (instance) => {
104
- const node = resolveHostNode(instance);
105
- nodeRef.current = node;
106
- animatedProps.setNativeView(node);
65
+ nodeRef.current = resolveHostNode(instance);
107
66
  assignRef(forwardedRef, instance);
108
67
  };
109
68
  // Reduced props are P-shaped (animated nodes already replaced by values); add the
package/build/render.js CHANGED
@@ -1,15 +1,37 @@
1
1
  // Mount a React element tree onto a Fabric surface. The native host hands us a
2
2
  // rootTag (via AppRegistry.registerRunnable); we create a surface for it and let
3
3
  // the reconciler drive shared, which commits into nativeFabricUIManager.
4
- import { createSurface, disposeRoot, setEventDispatcher, dlog, } from '@symbiote-native/engine';
4
+ import { createSurface, disposeRoot, setEventDispatcher, dlog, reportUncaughtError, } from '@symbiote-native/engine';
5
5
  import reconciler, { withDiscretePriority } from './host-config.js';
6
6
  import { LegacyRoot } from './reconciler-constants.js';
7
7
  const noop = () => { };
8
+ function errorReporter(origin) {
9
+ return (error, info) => {
10
+ reportUncaughtError(error, { origin, componentStack: info.componentStack });
11
+ };
12
+ }
13
+ const onUncaughtError = errorReporter('react render (no error boundary)');
14
+ // A boundary handled it, and it is still reported - the boundary decides what the USER sees, not
15
+ // whether the developer hears about it. Upstream's nativeOnCaughtError calls the same
16
+ // showErrorDialog as the uncaught path.
17
+ const onCaughtError = errorReporter('react render (caught by error boundary)');
18
+ // React recovered on its own (a concurrent render retried and succeeded), so this is not fatal -
19
+ // but it is still a real error the app swallowed a first render over. RN keeps React's
20
+ // defaultOnRecoverableError here, which reports it globally.
21
+ const onRecoverableError = errorReporter('react render (recovered)');
8
22
  // A native event runs the listener (which may call setState) outside React's
9
23
  // loop. Run it at discrete priority so the update takes the sync lane, then
10
24
  // flush that work synchronously to paint the result.
25
+ //
26
+ // Diagnostic seam (gated, perf investigation): every native event forces its own
27
+ // synchronous flush here, with no continuous-vs-discrete split (unlike DOM React,
28
+ // which lets high-frequency continuous events like drag/scroll coalesce). This
29
+ // dlog counts how many forced flushes one gesture (e.g. a Slider drag) produces,
30
+ // to compare against Vue/Angular's microtask-coalesced requestCommit(). Kept
31
+ // behind DEBUG per <keep_logs_gate_behind_DEBUG>, never removed.
11
32
  setEventDispatcher(run => {
12
33
  withDiscretePriority(run);
34
+ dlog('react event-dispatch: forced flushSyncWork');
13
35
  // @ts-expect-error flushSyncWork exists at runtime in react-reconciler 0.33
14
36
  reconciler.flushSyncWork();
15
37
  });
@@ -33,7 +55,11 @@ export function mount(rootTag, element) {
33
55
  // first) starts clean. Otherwise the stale container double-drives the surface.
34
56
  teardown(rootTag);
35
57
  const surface = createSurface(rootTag);
36
- const container = reconciler.createContainer(surface, LegacyRoot, null, false, null, 'symbiote', noop, noop, noop, noop, null);
58
+ const container = reconciler.createContainer(surface, LegacyRoot, null, false, null, 'symbiote', onUncaughtError, onCaughtError, onRecoverableError,
59
+ // onDefaultTransitionIndicator - the ONE that is genuinely a no-op. RN's own renderer says
60
+ // so in as many words: "Native doesn't have a default indicator" (ReactFabric.js's
61
+ // nativeOnDefaultTransitionIndicator).
62
+ noop, null);
37
63
  containers.set(rootTag, container);
38
64
  // react-reconciler 0.33 exposes updateContainerSync + flushSyncWork for an
39
65
  // immediate render/commit; @types 0.32 still lists the older updateContainer /
@@ -1,13 +1,8 @@
1
- // Re-exports @symbiote-native/css-parser verbatim from INSIDE this package, so a consuming app's own
2
- // require('@symbiote-native/react/metro-css-parser') resolves css-parser relative to THIS file's
3
- // location (adapters/react/node_modules, where css-parser IS a real dependency — see
4
- // package.json), not relative to the app's own node_modules, which does not and should not
5
- // need to declare @symbiote-native/css-parser itself. Node resolves each require() relative to the
6
- // requiring FILE's own location, so this indirection is what actually removes the extra install
7
- // step, not pnpm hoisting (which does not propagate this transitively across workspace packages
8
- // the way a flat classic node_modules would). .cjs, not .js: this package is "type": "module",
9
- // and Metro's babelTransformerPath loading expects a require()-able module.
10
- // createCssMetroTransformer is a factory, not a ready transformer — Metro's
11
- // babelTransformerPath needs the actual {transform, getCacheKey} object it returns,
12
- // not the css-parser package barrel.
1
+ // Re-exported from THIS file's own location so a consuming app's
2
+ // require('@symbiote-native/react/metro-css-parser') resolves css-parser as a dependency of THIS
3
+ // package, not the app's node_modules (Node resolves require() relative to the requiring file,
4
+ // not via pnpm hoisting) — the app never needs to declare @symbiote-native/css-parser itself.
5
+ // .cjs, not .js: this package is "type": "module", but Metro's babelTransformerPath loading needs
6
+ // a require()-able file. createCssMetroTransformer() is a factory — Metro needs the actual
7
+ // {transform, getCacheKey} object it returns, not the css-parser package barrel.
13
8
  module.exports = require('@symbiote-native/css-parser').createCssMetroTransformer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/react",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
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": {
@@ -37,8 +37,8 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "react-reconciler": "^0.33.0",
40
- "@symbiote-native/components": "0.2.6",
41
- "@symbiote-native/css-parser": "0.2.3"
40
+ "@symbiote-native/components": "0.4.0",
41
+ "@symbiote-native/css-parser": "0.3.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@symbiote-native/engine": ">=0.1.7",
@@ -49,7 +49,7 @@
49
49
  "@types/react": "^19.2.0",
50
50
  "@types/react-reconciler": "^0.32.2",
51
51
  "react": "19.2.3",
52
- "@symbiote-native/engine": "0.1.7",
52
+ "@symbiote-native/engine": "0.2.0",
53
53
  "@symbiote-native/test-utils": "0.1.6"
54
54
  },
55
55
  "scripts": {
@@ -1,2 +0,0 @@
1
- export { ActionSheetIOS } from '@symbiote-native/engine';
2
- export type { IActionSheetIOSOptions, IShareActionSheetIOSOptions, IShareActionSheetError, } from '@symbiote-native/engine';
@@ -1,4 +0,0 @@
1
- // ActionSheetIOS lives framework-agnostic in @symbiote-native/engine (imperative
2
- // native-bridge module, no visual, no lifecycle). The React adapter re-exports it
3
- // verbatim so the public surface is unchanged.
4
- export { ActionSheetIOS } from '@symbiote-native/engine';
@@ -1,2 +0,0 @@
1
- export { Alert } from '@symbiote-native/engine';
2
- export type { IAlertType, IAlertButtonStyle, IAlertButton, IAlertButtons, IAlertOptions, } from '@symbiote-native/engine';
@@ -1,5 +0,0 @@
1
- // Alert lives framework-agnostic in @symbiote-native/engine (imperative native-bridge
2
- // module, no visual, no lifecycle). The React adapter re-exports it verbatim so the
3
- // public surface is unchanged; the platform split (alert.ios/alert.android) lives
4
- // inside engine.
5
- export { Alert } from '@symbiote-native/engine';
@@ -1,2 +0,0 @@
1
- export { AppState } from '@symbiote-native/engine';
2
- export type { IAppStateStatus, IAppStateEvent } from '@symbiote-native/engine';
@@ -1,3 +0,0 @@
1
- // AppState moved to @symbiote-native/engine (a framework-agnostic device-state module re-exported
2
- // by every adapter). This thin re-export keeps @symbiote-native/react's public surface identical.
3
- export { AppState } from '@symbiote-native/engine';
@@ -1,2 +0,0 @@
1
- export { Appearance } from '@symbiote-native/engine';
2
- export type { IColorSchemeName, IColorSchemePreference } from '@symbiote-native/engine';
@@ -1,3 +0,0 @@
1
- // Appearance moved to @symbiote-native/engine (a framework-agnostic device-state module re-exported
2
- // by every adapter). This thin re-export keeps @symbiote-native/react's public surface identical.
3
- export { Appearance } from '@symbiote-native/engine';
@@ -1,2 +0,0 @@
1
- export { BackHandler } from '@symbiote-native/engine';
2
- export type { IBackPressEventName, IBackPressHandler } from '@symbiote-native/engine';
@@ -1,3 +0,0 @@
1
- // BackHandler moved to @symbiote-native/engine (a framework-agnostic device-state module re-exported
2
- // by every adapter). This thin re-export keeps @symbiote-native/react's public surface identical.
3
- export { BackHandler } from '@symbiote-native/engine';
@@ -1,2 +0,0 @@
1
- export { Dimensions } from '@symbiote-native/engine';
2
- export type { IDisplayMetrics, IDisplayMetricsAndroid, IDimensionsPayload, IDimensionsSet, IDimensionsKey, IDimensionsChangeListener, IDimensionsStatic, } from '@symbiote-native/engine';
@@ -1,3 +0,0 @@
1
- // Dimensions moved to @symbiote-native/engine (a framework-agnostic device-state module re-exported
2
- // by every adapter). This thin re-export keeps @symbiote-native/react's public surface identical.
3
- export { Dimensions } from '@symbiote-native/engine';
@@ -1,2 +0,0 @@
1
- export { I18nManager } from '@symbiote-native/engine';
2
- export type { II18nManagerConstants } from '@symbiote-native/engine';
@@ -1,4 +0,0 @@
1
- // I18nManager now lives framework-agnostic in @symbiote-native/engine (imperative
2
- // native-bridge module, no visual, no lifecycle). The React adapter re-exports it
3
- // verbatim so the public surface is unchanged.
4
- export { I18nManager } from '@symbiote-native/engine';
@@ -1,2 +0,0 @@
1
- export { Keyboard, KEYBOARD_EVENT } from '@symbiote-native/engine';
2
- export type { IKeyboardEventName, IKeyboardEvent, IKeyboardMetrics } from '@symbiote-native/engine';
@@ -1,3 +0,0 @@
1
- // Keyboard moved to @symbiote-native/engine (a framework-agnostic device-state module re-exported
2
- // by every adapter). This thin re-export keeps @symbiote-native/react's public surface identical.
3
- export { Keyboard, KEYBOARD_EVENT } from '@symbiote-native/engine';
@@ -1,2 +0,0 @@
1
- export { LayoutAnimation } from '@symbiote-native/engine';
2
- export type { ILayoutAnimationType, ILayoutAnimationProperty, ILayoutAnimationConfig, ILayoutAnimationAnim, ILayoutAnimationTypes, ILayoutAnimationProperties, } from '@symbiote-native/engine';
@@ -1,3 +0,0 @@
1
- // LayoutAnimation moved to @symbiote-native/engine (a framework-agnostic native-bridge consumer
2
- // re-exported by every adapter). This thin re-export keeps @symbiote-native/react's surface identical.
3
- export { LayoutAnimation } from '@symbiote-native/engine';
@@ -1,2 +0,0 @@
1
- export { Linking } from '@symbiote-native/engine';
2
- export type { IUrlEvent } from '@symbiote-native/engine';
@@ -1,4 +0,0 @@
1
- // Linking now lives framework-agnostic in @symbiote-native/engine (imperative native-bridge
2
- // module, no visual, no lifecycle). The React adapter re-exports it verbatim; the
3
- // platform split (linking.ios/linking.android) lives inside engine.
4
- export { Linking } from '@symbiote-native/engine';
@@ -1,2 +0,0 @@
1
- export { PermissionsAndroid, PERMISSIONS, RESULTS } from '@symbiote-native/engine';
2
- export type { IPermission, IPermissionStatus, IRationale } from '@symbiote-native/engine';
@@ -1,3 +0,0 @@
1
- // PermissionsAndroid moved to @symbiote-native/engine (a framework-agnostic device-state module
2
- // re-exported by every adapter). This thin re-export keeps @symbiote-native/react's surface identical.
3
- export { PermissionsAndroid, PERMISSIONS, RESULTS } from '@symbiote-native/engine';
@@ -1 +0,0 @@
1
- export { Settings } from '@symbiote-native/engine';
@@ -1,4 +0,0 @@
1
- // Settings now lives framework-agnostic in @symbiote-native/engine (imperative native-bridge
2
- // module, no visual, no lifecycle). The React adapter re-exports it verbatim so the
3
- // public surface is unchanged.
4
- export { Settings } from '@symbiote-native/engine';
@@ -1,2 +0,0 @@
1
- export { Share } from '@symbiote-native/engine';
2
- export type { IShareContent, IShareOptions, IShareAction } from '@symbiote-native/engine';
@@ -1,4 +0,0 @@
1
- // Share now lives framework-agnostic in @symbiote-native/engine (imperative native-bridge
2
- // module, no visual, no lifecycle). The React adapter re-exports it verbatim; the
3
- // platform split (share.ios/share.android) lives inside engine.
4
- export { Share } from '@symbiote-native/engine';
@@ -1 +0,0 @@
1
- export { currentlyFocusedInput, setInputFocused, setInputBlurred, blurTextInput, } from '@symbiote-native/engine';
@@ -1,3 +0,0 @@
1
- // TextInputState moved to @symbiote-native/engine (a framework-agnostic module: Keyboard.dismiss
2
- // and TextInput both reach it). This thin re-export keeps the adapter's import path stable.
3
- export { currentlyFocusedInput, setInputFocused, setInputBlurred, blurTextInput, } from '@symbiote-native/engine';
@@ -1 +0,0 @@
1
- export { ToastAndroid } from '@symbiote-native/engine';
@@ -1,4 +0,0 @@
1
- // ToastAndroid lives framework-agnostic in @symbiote-native/engine (imperative
2
- // native-bridge module, no visual, no lifecycle). The React adapter re-exports it
3
- // verbatim so the public surface is unchanged.
4
- export { ToastAndroid } from '@symbiote-native/engine';
@@ -1 +0,0 @@
1
- export { Vibration } from '@symbiote-native/engine';