@symbiote-native/react 3.0.1 → 3.0.3

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
@@ -22,43 +22,75 @@ adapter, not the core.
22
22
 
23
23
  ## Install
24
24
 
25
+ ```bash
26
+ npx @symbiote-native/cli new my-app --framework react
27
+ ```
28
+
29
+ One command, nothing to wire by hand: scaffolds the Metro config, the entry seam below, and
30
+ `@symbiote-native/react`/`react-native`/`react` as your app's own dependencies.
31
+
32
+ <details>
33
+ <summary>Manual install (no generator — an existing app, or you want to wire it yourself)</summary>
34
+
25
35
  ```bash
26
36
  npm install @symbiote-native/react react-native react
27
37
  ```
28
38
 
29
39
  `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. `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.
40
+ them, it only replaces the JS renderer that drives them. Follow [`examples/react`](../../examples/react)
41
+ for the Metro config and the entry seam below; there is no wiring script for an existing app.
42
+
43
+ </details>
33
44
 
34
45
  ---
35
46
 
36
47
  ## Use it
37
48
 
38
- The app is ordinary React — the native primitives are plain intrinsic tags, no import needed:
49
+ The app is ordinary React — the native primitives are plain intrinsic tags, no import needed.
50
+ Styling is a CSS class against a plain `.css` file, the convention every example app here
51
+ follows (`StyleSheet.create` still works, but isn't what any example uses):
39
52
 
40
53
  ```jsx
41
54
  import { useState } from 'react';
55
+ import './App.css';
42
56
 
43
57
  export default function App() {
44
58
  const [count, setCount] = useState(0);
45
59
  return (
46
- <view style={{ padding: 24 }}>
60
+ <safe-area-view className="screen">
47
61
  <text>Taps: {count}</text>
48
62
  <pressable onPress={() => setCount(c => c + 1)}>
49
63
  <text>Tap me</text>
50
64
  </pressable>
51
- </view>
65
+ </safe-area-view>
52
66
  );
53
67
  }
54
68
  ```
55
69
 
70
+ ```css
71
+ /* App.css */
72
+ .screen {
73
+ flex: 1;
74
+ align-items: center;
75
+ justify-content: center;
76
+ padding: 24px;
77
+ }
78
+ ```
79
+
80
+ <details>
81
+ <summary>Native entry point (index.js) — already scaffolded by <code>npx @symbiote-native/cli new</code></summary>
82
+
56
83
  The zero-config entry wires the RN-backed host seams (colors, images, device events,
57
84
  third-party ViewConfigs) and registers the app in one call — this is what
58
85
  [`examples/react`](../../examples/react) actually uses:
59
86
 
60
87
  ```js
61
88
  // index.js
89
+
90
+ // Registers host behaviors (Image, Pressable, Switch, ...) that /bootstrap alone doesn't
91
+ // reach; deleting this breaks them silently (Metro's production inlineRequires makes a
92
+ // side-effect-only barrel import go lazy, see register.ts).
93
+ import '@symbiote-native/react';
62
94
  import { registerApp } from '@symbiote-native/react/bootstrap';
63
95
  import App from './App';
64
96
  import { name as appName } from './app.json';
@@ -91,6 +123,8 @@ AppRegistry.registerRunnable(appName, ({ rootTag }) => {
91
123
  });
92
124
  ```
93
125
 
126
+ </details>
127
+
94
128
  Either way, the full canary ([`App.tsx`](../../examples/react/App.tsx)) exercises every block of
95
129
  the surface below.
96
130
 
@@ -141,8 +175,9 @@ the future scaffolder).
141
175
 
142
176
  ## Run it
143
177
 
144
- [`examples/react`](../../examples/react) is a stock React Native 0.86 app. Requires Node ≥ 22 and
145
- the [RN environment setup](https://reactnative.dev/docs/set-up-your-environment) (Xcode, CocoaPods):
178
+ [`examples/react`](../../examples/react) is a stock React Native 0.86 app. Requires Node ≥ 22.13
179
+ (react-native 0.86's own `package.json#engines`) and the [RN environment
180
+ setup](https://reactnative.dev/docs/set-up-your-environment) (Xcode, CocoaPods):
146
181
 
147
182
  ```bash
148
183
  cd examples/react
@@ -66,6 +66,8 @@ export interface IFlatListProps<ItemT> extends IAccessibilityProps, IAriaProps {
66
66
  style?: IStyleProp<IViewStyle>;
67
67
  contentContainerStyle?: IStyleProp<IViewStyle>;
68
68
  className?: string;
69
+ removeClippedSubviews?: boolean;
70
+ nestedScrollEnabled?: boolean;
69
71
  }
70
72
  export declare function FlatList<ItemT>(props: IFlatListProps<ItemT> & {
71
73
  ref?: Ref<IFlatListHandle>;
@@ -6,8 +6,8 @@
6
6
  // transforms are shared from @symbiote-native/components. This file only adapts to React's lifecycle
7
7
  // (element creation + ref threading).
8
8
  import { createElement, } from 'react';
9
- import { dlog, resolveClassName, } from '@symbiote-native/engine';
10
- import { SINGLE_COLUMN, chunkIntoRows, expandRowViewability, firstItemOfRow, lastItemOfRow, rowKeyExtractor, } from '@symbiote-native/components';
9
+ import { Platform, dlog, resolveClassName, } from '@symbiote-native/engine';
10
+ import { SINGLE_COLUMN, chunkIntoRows, expandRowViewability, firstItemOfRow, lastItemOfRow, removeClippedSubviewsOrDefault, rowKeyExtractor, } from '@symbiote-native/components';
11
11
  import { VirtualizedList, } from '../virtualized-list/index.js';
12
12
  export function FlatList(props) {
13
13
  const { ref, data, renderItem, keyExtractor, numColumns = SINGLE_COLUMN, columnWrapperStyle,
@@ -16,7 +16,12 @@ export function FlatList(props) {
16
16
  onViewableItemsChanged, viewabilityConfigCallbackPairs,
17
17
  // ItemSeparatorComponent is typed on ItemT; the multi-column stream is IRow<ItemT>, so it is
18
18
  // wrapped there to unwrap rows back to items, exactly like viewability above.
19
- ItemSeparatorComponent, ...rest } = props;
19
+ ItemSeparatorComponent, removeClippedSubviews: authoredRemoveClippedSubviews, ...ownRest } = props;
20
+ // FlatList.js always sends it, defaulted per platform.
21
+ const rest = {
22
+ ...ownRest,
23
+ removeClippedSubviews: removeClippedSubviewsOrDefault(authoredRemoveClippedSubviews, Platform.OS),
24
+ };
20
25
  dlog(`FlatList over ${data.length} items, ${numColumns} column(s)`);
21
26
  if (numColumns <= SINGLE_COLUMN) {
22
27
  return createElement((VirtualizedList), {
@@ -68,7 +68,7 @@ export const KeyboardAvoidingView = props => {
68
68
  for (const subscription of subscriptions)
69
69
  subscription.remove();
70
70
  };
71
- }, [behavior, keyboardVerticalOffset]);
71
+ }, [behavior, keyboardVerticalOffset, enabled]);
72
72
  const handleLayout = (event) => {
73
73
  const frame = readLayoutFrame(event.nativeEvent.layout);
74
74
  if (frame !== undefined) {
@@ -12,8 +12,8 @@
12
12
  // through the exit transition. The native exit-animation timing is what's deferred, not the
13
13
  // callback contract.
14
14
  import { createElement, useEffect, useReducer, } from 'react';
15
- import { dlog } from '@symbiote-native/engine';
16
- import { createInitialModalState, modalReducer, renderModal, resolveAccessibilityProps, shouldRenderModal, } from '@symbiote-native/components';
15
+ import { dlog, Platform } from '@symbiote-native/engine';
16
+ import { createInitialModalState, isModalVisible, modalReducer, modalVisibilityAction, renderModal, resolveAccessibilityProps, shouldRenderModal, } from '@symbiote-native/components';
17
17
  export const Modal = rawProps => {
18
18
  // Modal owns its host element (modal), so it folds aria/role here; the resolved fields
19
19
  // ride the host node via `...passthrough`. The events (onShow/onDismiss/onRequestClose/
@@ -21,18 +21,26 @@ export const Modal = rawProps => {
21
21
  // className is pulled out here, like style, and applied to the CONTAINER element below — left in
22
22
  // ...passthrough it would land on the outer modal host instead (renderModal composes
23
23
  // `style` into the container's style, not the host's).
24
- const { visible, transparent, backdropColor, animationType, presentationStyle, supportedOrientations, hardwareAccelerated, statusBarTranslucent, navigationBarTranslucent, allowSwipeDismissal, style, className, children, ...passthrough } = resolveAccessibilityProps(rawProps);
25
- // The iOS keep-alive: the effect runs AFTER this render, so a visible→hidden transition keeps the
26
- // node mounted for one frame (state.isRendered still true here) before the next render unmounts it,
27
- // the same shape as RN's componentDidUpdate setState (see state/modal.ts).
28
- const [state, dispatch] = useReducer(modalReducer, visible === true, createInitialModalState);
24
+ const { visible, transparent, backdropColor, animationType, presentationStyle, supportedOrientations, hardwareAccelerated, statusBarTranslucent, navigationBarTranslucent, allowSwipeDismissal, style, className, children, onDismiss, ...passthrough } = resolveAccessibilityProps(rawProps);
25
+ // The iOS keep-alive (state/modal.ts): armed on show, dropped only by the native dismiss below.
26
+ const isVisible = isModalVisible(visible);
27
+ const [state, dispatch] = useReducer(modalReducer, isVisible, createInitialModalState);
29
28
  useEffect(() => {
30
- dispatch(visible === true ? { type: 'show' } : { type: 'hide' });
31
- }, [visible]);
32
- if (!shouldRenderModal(visible === true, state)) {
29
+ const action = modalVisibilityAction(isVisible);
30
+ if (action !== undefined)
31
+ dispatch(action);
32
+ }, [isVisible]);
33
+ if (!shouldRenderModal(isVisible, state)) {
33
34
  dlog('Modal hidden -> no node committed');
34
35
  return null;
35
36
  }
37
+ // Modal.js: onDismiss is iOS-only — it drops the keep-alive, then tells the app.
38
+ const handleDismiss = () => {
39
+ if (Platform.OS !== 'ios')
40
+ return;
41
+ dispatch({ type: 'hide' });
42
+ onDismiss?.();
43
+ };
36
44
  const root = renderModal({
37
45
  visible,
38
46
  transparent,
@@ -45,7 +53,7 @@ export const Modal = rawProps => {
45
53
  navigationBarTranslucent,
46
54
  allowSwipeDismissal,
47
55
  style,
48
- passthrough,
56
+ passthrough: { ...passthrough, onDismiss: handleDismiss },
49
57
  });
50
58
  // root = modal > [container]; the user children nest UNDER the container View, never as
51
59
  // a direct sibling of the host (RN's modal content layout).
@@ -64,6 +64,8 @@ export interface IVirtualizedListProps<ItemT> extends IAccessibilityProps, IAria
64
64
  scrollEventThrottle?: number;
65
65
  keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
66
66
  keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
67
+ removeClippedSubviews?: boolean;
68
+ nestedScrollEnabled?: boolean;
67
69
  style?: IStyleProp<IViewStyle>;
68
70
  contentContainerStyle?: IStyleProp<IViewStyle>;
69
71
  className?: string;
@@ -146,7 +146,7 @@ export function VirtualizedList(props) {
146
146
  }
147
147
  dlog(`VirtualizedList scrollTo offset=${clamped} pending-ref (horizontal=${isHorizontal})`);
148
148
  setCommandedOffset(target);
149
- }, []);
149
+ }, [scrollHandle]);
150
150
  // dispatch and runEffects are mutually recursive (a schedule-refill / fire-viewable effect
151
151
  // dispatches a follow-up action), so runEffects reaches dispatch through a ref.
152
152
  const dispatchRef = useRef(() => { });
@@ -491,8 +491,9 @@ export function VirtualizedList(props) {
491
491
  const resolvedContentContainerStyle = horizontal
492
492
  ? [contentContainerStyle, { width: total }]
493
493
  : contentContainerStyle;
494
+ // VirtualizedList.js: `[inversionStyle, style]` — the app's style can override the flip.
494
495
  const resolvedStyle = inverted
495
- ? [style, horizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE]
496
+ ? [horizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE, style]
496
497
  : style;
497
498
  const scrollProps = {
498
499
  // The list's accessibility surface rides down onto the ScrollView. Spread first so the
@@ -507,6 +508,9 @@ export function VirtualizedList(props) {
507
508
  // scrolls the two axes with different native ViewManagers. The behavior's own fold deletes the
508
509
  // prop and rewrites it from the tag, so passing it would be at best redundant and at worst a
509
510
  // contradiction it has to warn about.
511
+ // VirtualizedList.js:1111 — Android moves the scrollbar back after the `scale: -1` flip.
512
+ if (inverted)
513
+ scrollProps.isInvertedVirtualizedList = true;
510
514
  if (onScrollBeginDrag !== undefined)
511
515
  scrollProps.onScrollBeginDrag = onScrollBeginDrag;
512
516
  if (onScrollEndDrag !== undefined)
@@ -62,6 +62,8 @@ export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps
62
62
  scrollEventThrottle?: number;
63
63
  keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
64
64
  keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
65
+ removeClippedSubviews?: boolean;
66
+ nestedScrollEnabled?: boolean;
65
67
  style?: IStyleProp<IViewStyle>;
66
68
  contentContainerStyle?: IStyleProp<IViewStyle>;
67
69
  className?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/react",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
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": "3.0.1",
61
- "@symbiote-native/css-parser": "0.5.0"
60
+ "@symbiote-native/components": "3.1.1",
61
+ "@symbiote-native/css-parser": "0.5.1"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "react": ">=19.0.0",
65
65
  "react-native": ">=0.86",
66
- "@symbiote-native/engine": "^1.2.0"
66
+ "@symbiote-native/engine": "^1.3.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": "1.2.0",
73
- "@symbiote-native/test-utils": "0.4.1"
72
+ "@symbiote-native/engine": "1.3.0",
73
+ "@symbiote-native/test-utils": "0.4.3"
74
74
  },
75
75
  "scripts": {
76
76
  "typecheck": "tsc --build",