@symbiote-native/react 0.1.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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/build/components/activity-indicator/index.android.d.ts +2 -0
  4. package/build/components/activity-indicator/index.android.js +13 -0
  5. package/build/components/activity-indicator/index.d.ts +1 -0
  6. package/build/components/activity-indicator/index.ios.d.ts +2 -0
  7. package/build/components/activity-indicator/index.ios.js +10 -0
  8. package/build/components/activity-indicator/index.js +4 -0
  9. package/build/components/activity-indicator/shared.d.ts +7 -0
  10. package/build/components/activity-indicator/shared.js +20 -0
  11. package/build/components/button.d.ts +6 -0
  12. package/build/components/button.js +42 -0
  13. package/build/components/drawer-layout-android/index.android.d.ts +3 -0
  14. package/build/components/drawer-layout-android/index.android.js +93 -0
  15. package/build/components/drawer-layout-android/index.d.ts +3 -0
  16. package/build/components/drawer-layout-android/index.js +19 -0
  17. package/build/components/drawer-layout-android/shared.d.ts +19 -0
  18. package/build/components/drawer-layout-android/shared.js +7 -0
  19. package/build/components/flat-list/index.d.ts +73 -0
  20. package/build/components/flat-list/index.js +86 -0
  21. package/build/components/image/index.d.ts +9 -0
  22. package/build/components/image/index.js +28 -0
  23. package/build/components/image-background/index.d.ts +10 -0
  24. package/build/components/image-background/index.js +46 -0
  25. package/build/components/input-accessory-view/index.d.ts +11 -0
  26. package/build/components/input-accessory-view/index.js +14 -0
  27. package/build/components/keyboard-avoiding-view/index.d.ts +17 -0
  28. package/build/components/keyboard-avoiding-view/index.js +85 -0
  29. package/build/components/modal/index.d.ts +24 -0
  30. package/build/components/modal/index.js +56 -0
  31. package/build/components/pressable/index.d.ts +30 -0
  32. package/build/components/pressable/index.js +100 -0
  33. package/build/components/refresh-control/index.d.ts +17 -0
  34. package/build/components/refresh-control/index.js +21 -0
  35. package/build/components/safe-area-view/index.d.ts +11 -0
  36. package/build/components/safe-area-view/index.js +18 -0
  37. package/build/components/scroll-view/index.android.d.ts +3 -0
  38. package/build/components/scroll-view/index.android.js +53 -0
  39. package/build/components/scroll-view/index.d.ts +1 -0
  40. package/build/components/scroll-view/index.ios.d.ts +3 -0
  41. package/build/components/scroll-view/index.ios.js +27 -0
  42. package/build/components/scroll-view/index.js +4 -0
  43. package/build/components/scroll-view/shared.d.ts +98 -0
  44. package/build/components/scroll-view/shared.js +195 -0
  45. package/build/components/scroll-view/sticky-header.d.ts +9 -0
  46. package/build/components/scroll-view/sticky-header.js +161 -0
  47. package/build/components/section-list/index.d.ts +8 -0
  48. package/build/components/section-list/index.js +12 -0
  49. package/build/components/switch/index.android.d.ts +2 -0
  50. package/build/components/switch/index.android.js +12 -0
  51. package/build/components/switch/index.d.ts +1 -0
  52. package/build/components/switch/index.ios.d.ts +2 -0
  53. package/build/components/switch/index.ios.js +10 -0
  54. package/build/components/switch/index.js +6 -0
  55. package/build/components/switch/shared.d.ts +10 -0
  56. package/build/components/switch/shared.js +59 -0
  57. package/build/components/text-input/index.d.ts +50 -0
  58. package/build/components/text-input/index.js +167 -0
  59. package/build/components/touchable/index.d.ts +20 -0
  60. package/build/components/touchable/index.js +95 -0
  61. package/build/components/touchable-native-feedback/index.d.ts +18 -0
  62. package/build/components/touchable-native-feedback/index.js +32 -0
  63. package/build/components/virtualized-list/index.d.ts +73 -0
  64. package/build/components/virtualized-list/index.js +552 -0
  65. package/build/components/virtualized-section-list/index.d.ts +66 -0
  66. package/build/components/virtualized-section-list/index.js +99 -0
  67. package/build/components.d.ts +54 -0
  68. package/build/components.js +17 -0
  69. package/build/create-portal.d.ts +4 -0
  70. package/build/create-portal.js +23 -0
  71. package/build/create-tunnel.d.ts +12 -0
  72. package/build/create-tunnel.js +60 -0
  73. package/build/descriptor-to-react/index.d.ts +3 -0
  74. package/build/descriptor-to-react/index.js +15 -0
  75. package/build/hooks/use-color-scheme.d.ts +2 -0
  76. package/build/hooks/use-color-scheme.js +13 -0
  77. package/build/hooks/use-window-dimensions.d.ts +2 -0
  78. package/build/hooks/use-window-dimensions.js +30 -0
  79. package/build/host-config.d.ts +7 -0
  80. package/build/host-config.js +169 -0
  81. package/build/host-instance.d.ts +3 -0
  82. package/build/host-instance.js +20 -0
  83. package/build/index.d.ts +98 -0
  84. package/build/index.js +85 -0
  85. package/build/modules/accessibility-info/index.android.d.ts +2 -0
  86. package/build/modules/accessibility-info/index.android.js +4 -0
  87. package/build/modules/accessibility-info/index.d.ts +1 -0
  88. package/build/modules/accessibility-info/index.ios.d.ts +2 -0
  89. package/build/modules/accessibility-info/index.ios.js +3 -0
  90. package/build/modules/accessibility-info/index.js +5 -0
  91. package/build/modules/accessibility-info/shared.d.ts +1 -0
  92. package/build/modules/accessibility-info/shared.js +1 -0
  93. package/build/modules/action-sheet-ios.d.ts +2 -0
  94. package/build/modules/action-sheet-ios.js +4 -0
  95. package/build/modules/alert.d.ts +2 -0
  96. package/build/modules/alert.js +5 -0
  97. package/build/modules/animated/create-animated-component.d.ts +12 -0
  98. package/build/modules/animated/create-animated-component.js +125 -0
  99. package/build/modules/animated/index.d.ts +52 -0
  100. package/build/modules/animated/index.js +84 -0
  101. package/build/modules/app-registry/index.d.ts +7 -0
  102. package/build/modules/app-registry/index.js +26 -0
  103. package/build/modules/app-state.d.ts +2 -0
  104. package/build/modules/app-state.js +3 -0
  105. package/build/modules/appearance.d.ts +2 -0
  106. package/build/modules/appearance.js +3 -0
  107. package/build/modules/back-handler.d.ts +2 -0
  108. package/build/modules/back-handler.js +3 -0
  109. package/build/modules/dimensions.d.ts +2 -0
  110. package/build/modules/dimensions.js +3 -0
  111. package/build/modules/i18n-manager.d.ts +2 -0
  112. package/build/modules/i18n-manager.js +4 -0
  113. package/build/modules/keyboard.d.ts +2 -0
  114. package/build/modules/keyboard.js +3 -0
  115. package/build/modules/layout-animation.d.ts +2 -0
  116. package/build/modules/layout-animation.js +3 -0
  117. package/build/modules/linking.d.ts +2 -0
  118. package/build/modules/linking.js +4 -0
  119. package/build/modules/permissions-android.d.ts +2 -0
  120. package/build/modules/permissions-android.js +3 -0
  121. package/build/modules/settings.d.ts +1 -0
  122. package/build/modules/settings.js +4 -0
  123. package/build/modules/share.d.ts +2 -0
  124. package/build/modules/share.js +4 -0
  125. package/build/modules/status-bar/index.android.d.ts +3 -0
  126. package/build/modules/status-bar/index.android.js +89 -0
  127. package/build/modules/status-bar/index.d.ts +1 -0
  128. package/build/modules/status-bar/index.ios.d.ts +3 -0
  129. package/build/modules/status-bar/index.ios.js +64 -0
  130. package/build/modules/status-bar/index.js +4 -0
  131. package/build/modules/status-bar/shared.d.ts +6 -0
  132. package/build/modules/status-bar/shared.js +6 -0
  133. package/build/modules/text-input-state.d.ts +1 -0
  134. package/build/modules/text-input-state.js +3 -0
  135. package/build/modules/toast-android.d.ts +1 -0
  136. package/build/modules/toast-android.js +4 -0
  137. package/build/modules/vibration.d.ts +1 -0
  138. package/build/modules/vibration.js +4 -0
  139. package/build/reconciler-constants.d.ts +7 -0
  140. package/build/reconciler-constants.js +12 -0
  141. package/build/render.d.ts +7 -0
  142. package/build/render.js +60 -0
  143. package/build/utils/responder-props.d.ts +1 -0
  144. package/build/utils/responder-props.js +1 -0
  145. package/build/utils/styles.d.ts +1 -0
  146. package/build/utils/styles.js +1 -0
  147. package/metro-css-parser.cjs +11 -0
  148. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # @symbiote-native/react
2
+
3
+ The **React adapter** for [SymbioteJS](../../README.md) — drive real native iOS/Android views from
4
+ React, with React Native's own renderer never in the path. It is a `react-reconciler` host config
5
+ in **mutation mode** (`appendChild` / `insertBefore` / `removeChild` → the engine's four-call
6
+ mutation API); `@symbiote-native/engine` does the clone-on-write commit into Fabric.
7
+
8
+ This is the **reference adapter**: the known-good driver used to validate the native pipe and the
9
+ commit engine before any other framework lands, so a break in Vue/Svelte/Solid isolates to *that*
10
+ adapter, not the core.
11
+
12
+ <div align="center">
13
+
14
+ ![React driving real native iOS views through SymbioteJS](../../assets/react-demo.gif)
15
+
16
+ </div>
17
+
18
+ > New to SymbioteJS? The [root README](../../README.md) has the architecture and the one fact it
19
+ > rests on — React is just *one client* of `nativeFabricUIManager`.
20
+
21
+ ---
22
+
23
+ ## Use it
24
+
25
+ The app is ordinary React — it imports primitives from `@symbiote-native/react`, not `react-native`:
26
+
27
+ ```jsx
28
+ import { useState } from 'react';
29
+ import { View, Text, Pressable } from '@symbiote-native/react';
30
+
31
+ export default function App() {
32
+ const [count, setCount] = useState(0);
33
+ return (
34
+ <View style={{ padding: 24 }}>
35
+ <Text>Taps: {count}</Text>
36
+ <Pressable onPress={() => setCount((c) => c + 1)}>
37
+ <Text>Tap me</Text>
38
+ </Pressable>
39
+ </View>
40
+ );
41
+ }
42
+ ```
43
+
44
+ The native entry registers a low-level *runnable* instead of a React component: RN's Fabric host
45
+ calls it with the surface's `rootTag`, and the renderer takes over from there — `nativeFabric-
46
+ UIManager` is driven directly, RN's own renderer never runs.
47
+
48
+ ```js
49
+ // index.js
50
+ import { AppRegistry, processColor } from 'react-native';
51
+ import { createElement } from 'react';
52
+ import { mount } from '@symbiote-native/react';
53
+ import { setColorProcessor } from '@symbiote-native/engine';
54
+ import App from './App';
55
+ import { name as appName } from './app.json';
56
+
57
+ // Colors reach Fabric as platform ints; let the engine use RN's own converter.
58
+ setColorProcessor(processColor);
59
+
60
+ // registerRunnable (not registerComponent): RN stores a raw mount callback and never renders
61
+ // it with its own renderer.
62
+ AppRegistry.registerRunnable(appName, ({ rootTag }) => {
63
+ mount(rootTag, createElement(App));
64
+ });
65
+ ```
66
+
67
+ The runnable example is [`examples/react`](../../examples/react) — a stock RN 0.86 app whose full
68
+ canary ([`App.tsx`](../../examples/react/App.tsx)) exercises every block of the surface below.
69
+
70
+ ---
71
+
72
+ ## Surface — verified on device, iOS + Android
73
+
74
+ Every interaction commits through `@symbiote-native/engine` into Fabric, with RN's renderer never in the
75
+ path (R1 + R2 + R3 — decision record 0009):
76
+
77
+ - **Primitives** — `View` · `Text` · `Image` · `ImageBackground` · `ScrollView` · `TextInput` ·
78
+ `Pressable` · `Touchable*` · `Button` · `Switch` · `Modal` · `ActivityIndicator` ·
79
+ `SafeAreaView` · `RefreshControl` · `FlatList` · `SectionList` · `VirtualizedList`.
80
+ - **Runtime modules** — `Platform` · `StyleSheet` (incl. `hairlineWidth`) · `Dimensions` ·
81
+ `Appearance` · `PixelRatio` · `AppState`, plus imperative `Alert` · `ActionSheetIOS` · `Share` ·
82
+ `Linking` · `Vibration` · `Keyboard` · `StatusBar` — each reaching its real native module on the
83
+ bridgeless host.
84
+ - **`Animated`, both drivers** — JS *and* native driver side by side (`timing` · `spring` · `loop` ·
85
+ `interpolate` · `ValueXY` · tracking · `diffClamp`). Native offload is proven by jamming the JS
86
+ thread 1.5 s: the native-driven animations keep moving, the JS-driven one stalls (ADR 0016 · 0017).
87
+ - **Third-party native views** — `@react-native-community/slider` used straight from the package
88
+ with zero SymbioteJS metadata; the engine derives its events and prop processors from the library's
89
+ own ViewConfig at runtime — the "install the package, use its component" path.
90
+ - **Gestures & events** — the responder lifecycle (grant/move/release/terminate, LCA-scoped
91
+ re-negotiation), two-phase capture→bubble delivery, `Pressable` press-retention, `Touchable*`
92
+ delays, and `PanResponder` — all in the engine.
93
+ - **Accessibility** — the `accessibility*` / ARIA prop layer (roles, labels, states, focus,
94
+ grouping) verified against the platform a11y tree on both platforms.
95
+ - **Modern styling** — RN's JS style processors (`boxShadow` · `filter` · `transform` ·
96
+ `transformOrigin` · `aspectRatio` · `fontVariant`) and color resolution run in the engine, so
97
+ CSS-style props commit correctly on **both** platforms — not just iOS tolerating a raw string
98
+ while Android's strict native converter crashes on it.
99
+
100
+ ### Android
101
+
102
+ The same canary runs on an Android emulator through the same `@symbiote-native/engine` core. Two signals
103
+ RN ties to a view host SymbioteJS bypasses — or never shipped on Android — are re-supplied by a small
104
+ `@symbiote-native/android` native package (`KeyboardObserver` host shim; `SettingsManager` →
105
+ `SharedPreferences`). `Platform` and the component-name map are Metro-split per OS (`.ios` /
106
+ `.android`), so there is no `Platform.OS` runtime branch. iOS stays the reference surface (more
107
+ real-hardware time, wider prop-edge coverage); `ActionSheetIOS` is iOS-only by design, and
108
+ `Vibration` needs the app to declare the `VIBRATE` permission (RN's standard requirement, owned by
109
+ the future scaffolder).
110
+
111
+ ---
112
+
113
+ ## Run it
114
+
115
+ [`examples/react`](../../examples/react) is a stock React Native 0.86 app. Requires Node ≥ 22 and
116
+ the [RN environment setup](https://reactnative.dev/docs/set-up-your-environment) (Xcode, CocoaPods):
117
+
118
+ ```bash
119
+ cd examples/react
120
+ npm install
121
+ bundle install # first time only — installs CocoaPods itself
122
+ bundle exec pod install # (cd ios && pod install) — fetch native pods
123
+
124
+ # terminal 1 — Metro. DEBUG=1 turns on diagnostic logs (Babel inlines it).
125
+ DEBUG=1 npm start --reset-cache
126
+
127
+ # terminal 2 — build + launch
128
+ npm run ios # iOS simulator (the reference surface)
129
+ npm run android # Android emulator (manual-links @symbiote-native/android)
130
+ ```
131
+
132
+ Press <kbd>R</kbd> in the simulator to reload. Because `DEBUG` is Babel-inlined into the bundle,
133
+ changing it requires restarting Metro with `--reset-cache`.
134
+
135
+ ---
136
+
137
+ ## Test it
138
+
139
+ ```bash
140
+ pnpm test # vitest, from the workspace root — headless, fake Fabric slot
141
+
142
+ cd examples/react
143
+ npm run e2e:build:ios # build the app for Detox (once per native change)
144
+ npm run e2e:test:ios # run the canary journeys on the iOS simulator
145
+ # …or the android equivalents: e2e:build:android / e2e:test:android
146
+ ```
147
+
148
+ Why these come for free — a SymbioteJS app is a stock RN app underneath, so RN's whole testing
149
+ ecosystem applies unchanged. See [Testing](../../README.md#testing).
@@ -0,0 +1,2 @@
1
+ export type { IActivityIndicatorProps } from './shared';
2
+ export declare const ActivityIndicator: import("react").FC<import("./shared").IActivityIndicatorProps>;
@@ -0,0 +1,13 @@
1
+ // ActivityIndicator on Android: AndroidProgressBar needs `styleAttr` (it drives the
2
+ // ProgressBar's setStyle(); without it the view throws "setStyle() not called") and
3
+ // `indeterminate: true`, and its default color is the theme (null), per RN's
4
+ // ActivityIndicator.js android branch ({styleAttr:'Normal', indeterminate:true}). Metro
5
+ // picks this on an Android host; no Platform.OS read. See ADR 0020.
6
+ // device-verify-pending: prop names mirror RN's AndroidProgressBar, proven on a real
7
+ // host by the absence of the "setStyle() not called" red box.
8
+ import { createActivityIndicator } from './shared';
9
+ export const ActivityIndicator = createActivityIndicator({
10
+ // RN: `color = Platform.OS === 'ios' ? GRAY : null`; Android lets the theme color it.
11
+ defaultColor: null,
12
+ nativeExtras: { styleAttr: 'Normal', indeterminate: true },
13
+ });
@@ -0,0 +1 @@
1
+ export * from './index.ios';
@@ -0,0 +1,2 @@
1
+ export type { IActivityIndicatorProps } from './shared';
2
+ export declare const ActivityIndicator: import("react").FC<import("./shared").IActivityIndicatorProps>;
@@ -0,0 +1,10 @@
1
+ // ActivityIndicator on iOS: RCTActivityIndicatorView takes the size enum + a GRAY
2
+ // default color and no extra native props. Also the base (activity-indicator.ts
3
+ // re-exports it) for headless / web. See ADR 0020.
4
+ import { createActivityIndicator } from './shared';
5
+ // RN's iOS default spinner color (Libraries/.../ActivityIndicator.js GRAY).
6
+ const IOS_DEFAULT_COLOR = '#999999';
7
+ export const ActivityIndicator = createActivityIndicator({
8
+ defaultColor: IOS_DEFAULT_COLOR,
9
+ nativeExtras: {},
10
+ });
@@ -0,0 +1,4 @@
1
+ // Base / default ActivityIndicator: re-exports the iOS build. Metro overrides this with
2
+ // activity-indicator.ios.ts / .android.ts on a real host; under tsx / tsc / web the host
3
+ // config resolves here. Filename is the selector, no Platform.OS read. See ADR 0020.
4
+ export * from './index.ios';
@@ -0,0 +1,7 @@
1
+ import type { FC } from 'react';
2
+ import type { IActivityIndicatorPlatform, IActivityIndicatorProps as IActivityIndicatorBaseProps } from '@symbiote-native/components';
3
+ export type { IActivityIndicatorPlatform } from '@symbiote-native/components';
4
+ export type IActivityIndicatorProps = IActivityIndicatorBaseProps & {
5
+ className?: string;
6
+ };
7
+ export declare function createActivityIndicator(platform: IActivityIndicatorPlatform): FC<IActivityIndicatorProps>;
@@ -0,0 +1,20 @@
1
+ // ActivityIndicator is the React lifecycle half. The render (size translation, wrapper +
2
+ // spinner, color omission) lives framework-agnostic in @symbiote-native/components; here the
3
+ // adapter only resolves props (accessibility, defaults), calls the render fn, and bridges
4
+ // the Descriptor to a React element. No state, no effects: this component is render-only.
5
+ //
6
+ // The per-platform bits (iOS GRAY default + no extras; Android theme/null + styleAttr +
7
+ // indeterminate) are supplied by the .ios/.android files via `IActivityIndicatorPlatform`.
8
+ import { renderActivityIndicator, resolveAccessibilityProps } from '@symbiote-native/components';
9
+ import { descriptorToReact } from '../../descriptor-to-react';
10
+ export function createActivityIndicator(platform) {
11
+ return rawProps => {
12
+ // The wrapper is a raw symbiote-view, not the View FC, so it never runs
13
+ // resolveAccessibilityProps itself, so fold aria/role here, then forward the resolved
14
+ // accessibility* surface (plus testID / onLayout) onto the wrapper via `passthrough`.
15
+ const props = resolveAccessibilityProps(rawProps);
16
+ const { animating = true, color, hidesWhenStopped = true, size = 'small', style, ...passthrough } = props;
17
+ const descriptor = renderActivityIndicator({ animating, color, hidesWhenStopped, size, style, passthrough }, platform);
18
+ return descriptorToReact(descriptor);
19
+ };
20
+ }
@@ -0,0 +1,6 @@
1
+ import { type FC } from 'react';
2
+ import type { IButtonProps as IButtonBaseProps } from '@symbiote-native/components';
3
+ export type IButtonProps = IButtonBaseProps & {
4
+ className?: string;
5
+ };
6
+ export declare const Button: FC<IButtonProps>;
@@ -0,0 +1,42 @@
1
+ // Button is the minimal cross-platform button, rendered in its iOS shape per RN's Button.js: a
2
+ // TouchableOpacity wrapping a Text. The base text style, the role constant, and the color fold
3
+ // (caller color tints the label; disabled greys it) are shared in @symbiote-native/components/view; here
4
+ // React only composes its TouchableOpacity + Text and forwards the native-only props.
5
+ import { createElement } from 'react';
6
+ import { Text } from '../components';
7
+ import { TouchableOpacity } from './touchable';
8
+ import { BUTTON_ACCESSIBILITY_ROLE, resolveButtonTextStyle } from '@symbiote-native/components';
9
+ export const Button = props => {
10
+ const { title, onPress, color, disabled, touchSoundDisabled, testID, hasTVPreferredFocus, nextFocusDown, nextFocusForward, nextFocusLeft, nextFocusRight, nextFocusUp, ...accessibilityRest } = props;
11
+ const textStyle = resolveButtonTextStyle(color, disabled);
12
+ // The pressable / native View props TouchableOpacity does not type but forwards to Fabric
13
+ // (testID + TV-focus). Carried as a plain record (the pass-through idiom Image uses) so
14
+ // excess-property typing does not reject the native-only keys. TV-focus is inert on a phone.
15
+ const nativeForward = { testID };
16
+ if (hasTVPreferredFocus !== undefined)
17
+ nativeForward.hasTVPreferredFocus = hasTVPreferredFocus;
18
+ if (nextFocusDown !== undefined)
19
+ nativeForward.nextFocusDown = nextFocusDown;
20
+ if (nextFocusForward !== undefined)
21
+ nativeForward.nextFocusForward = nextFocusForward;
22
+ if (nextFocusLeft !== undefined)
23
+ nativeForward.nextFocusLeft = nextFocusLeft;
24
+ if (nextFocusRight !== undefined)
25
+ nativeForward.nextFocusRight = nextFocusRight;
26
+ if (nextFocusUp !== undefined)
27
+ nativeForward.nextFocusUp = nextFocusUp;
28
+ // RN's Button sets role=button, is accessible, and propagates the disabled accessibility state.
29
+ // The caller's accessibility props pass through, but Button's fixed role / accessible /
30
+ // disabled-state win, applied after the spread. touchSoundDisabled maps to the pressable's
31
+ // android_disableSound.
32
+ return createElement(TouchableOpacity, {
33
+ ...accessibilityRest,
34
+ ...nativeForward,
35
+ onPress,
36
+ disabled,
37
+ android_disableSound: touchSoundDisabled,
38
+ accessibilityRole: BUTTON_ACCESSIBILITY_ROLE,
39
+ accessible: true,
40
+ accessibilityState: { disabled },
41
+ }, createElement(Text, { style: textStyle }, title));
42
+ };
@@ -0,0 +1,3 @@
1
+ import type { IDrawerLayoutAndroidHandle, IDrawerLayoutAndroidProps } from './shared';
2
+ export type { IDrawerPosition, IDrawerLockMode, IKeyboardDismissMode, IDrawerState, IDrawerSlideEvent, IDrawerLayoutAndroidProps, IDrawerLayoutAndroidHandle, } from './shared';
3
+ export declare const DrawerLayoutAndroid: import("react").ForwardRefExoticComponent<IDrawerLayoutAndroidProps & import("react").RefAttributes<IDrawerLayoutAndroidHandle>>;
@@ -0,0 +1,93 @@
1
+ // DrawerLayoutAndroid: the real Android build. AndroidDrawerLayout is an ordinary
2
+ // Fabric host node: it lives in the SAME childSet and commits through the SAME
3
+ // completeRoot as the rest of the tree, exactly like Modal/Switch. There is no
4
+ // per-library glue; shared derives the view's events/processors from its ViewConfig,
5
+ // so we render the RAW Fabric name `AndroidDrawerLayout` (the derive-by-default path:
6
+ // any non-`symbiote-*` createElement type flows through untouched).
7
+ //
8
+ // Behavior mirrors RN's DrawerLayoutAndroid.android.js: the navigation view (from
9
+ // `renderNavigationView()`) and the main content are wrapped in two container Views,
10
+ // and, critically, the child order RN commits is [mainSubview, drawerSubview]: the
11
+ // content wrapper FIRST, the navigation wrapper SECOND (android render:
12
+ // {childrenWrapper}{drawerViewWrapper}). The navigation wrapper is absolutely
13
+ // positioned, drawerWidth-wide, and gated by pointerEvents so it stays untouchable
14
+ // until opened. `openDrawer`/`closeDrawer` are imperative, reached through a ref, and
15
+ // dispatched as the `openDrawer`/`closeDrawer` view commands against the host node,
16
+ // mirroring Switch's dispatchViewCommand path (Commands.openDrawer/closeDrawer in
17
+ // AndroidDrawerLayoutNativeComponent).
18
+ //
19
+ // The platform-invariant math — the host prop bag + the content/navigation wrapper styles, the
20
+ // slide/state event normalization, the imperative open/close handle, and the view/command NAMES —
21
+ // lives in @symbiote/components, shared verbatim with the Vue adapter; here React supplies only the
22
+ // lifecycle: a ref holds the host node, a `drawerOpened` state gates the navigation wrapper's
23
+ // pointerEvents, and useImperativeHandle wires the imperative handle.
24
+ //
25
+ // Metro picks this file on an Android host; off Android the base index.ts
26
+ // renders the fallback. No Platform.OS read; the filename is the selector (ADR 0019).
27
+ // device-verify-pending: the `AndroidDrawerLayout` name + the openDrawer/closeDrawer
28
+ // commands are RN-source-confirmed, not yet exercised on a real Android host.
29
+ import { createElement, forwardRef, useCallback, useImperativeHandle, useRef, useState, } from 'react';
30
+ import { dlog } from '@symbiote/engine';
31
+ import { buildDrawerHandle, DEFAULT_DRAWER_POSITION, offsetFromSlide, resolveAccessibilityProps, resolveDrawerLayout, stateFromChange, } from '@symbiote/components';
32
+ import { View } from '../../components';
33
+ export const DrawerLayoutAndroid = forwardRef((props, ref) => {
34
+ const { drawerWidth, drawerPosition, drawerLockMode, keyboardDismissMode, drawerBackgroundColor, statusBarBackgroundColor, onDrawerOpen, onDrawerClose, onDrawerSlide, onDrawerStateChanged, renderNavigationView, style, children, ...passthrough } = props;
35
+ const node = useRef(null);
36
+ // RN tracks drawerOpened to gate the navigation view's pointerEvents (android
37
+ // _onDrawerOpen/_onDrawerClose setState). Closed -> 'none' so the off-screen drawer
38
+ // never intercepts touches; open -> 'auto' (folded in by resolveDrawerLayout).
39
+ const [drawerOpened, setDrawerOpened] = useState(false);
40
+ // The imperative handle reads the node through a LAZY getter (() => node.current), not the node
41
+ // captured once: it is null until the element commits. The React twin of Vue's
42
+ // expose(buildDrawerHandle(…)).
43
+ useImperativeHandle(ref, () => buildDrawerHandle(() => node.current), []);
44
+ const handleDrawerOpen = useCallback(() => {
45
+ dlog('DrawerLayoutAndroid onDrawerOpen');
46
+ setDrawerOpened(true);
47
+ onDrawerOpen?.();
48
+ }, [onDrawerOpen]);
49
+ const handleDrawerClose = useCallback(() => {
50
+ dlog('DrawerLayoutAndroid onDrawerClose');
51
+ setDrawerOpened(false);
52
+ onDrawerClose?.();
53
+ }, [onDrawerClose]);
54
+ const handleDrawerSlide = useCallback((event) => {
55
+ onDrawerSlide?.({ offset: offsetFromSlide(event) });
56
+ }, [onDrawerSlide]);
57
+ const handleDrawerStateChanged = useCallback((event) => {
58
+ onDrawerStateChanged?.(stateFromChange(event));
59
+ }, [onDrawerStateChanged]);
60
+ const resolved = resolveDrawerLayout({
61
+ drawerWidth,
62
+ drawerPosition,
63
+ drawerLockMode,
64
+ keyboardDismissMode,
65
+ drawerBackgroundColor,
66
+ statusBarBackgroundColor,
67
+ drawerOpened,
68
+ style,
69
+ // The drawer renders a raw host node (not the View FC), so unlike View-backed components nothing
70
+ // else resolves aria-*/role — fold them into accessibility* here before passing through.
71
+ passthrough: resolveAccessibilityProps(passthrough),
72
+ });
73
+ // RN's mainSubview: content wrapped in an absolute box.
74
+ const contentWrapper = createElement(View, { style: resolved.contentWrapperStyle }, children);
75
+ // RN's drawerSubview: the navigation view wrapped, drawerWidth-wide, painted with
76
+ // drawerBackgroundColor, untouchable until opened.
77
+ const navigationWrapper = createElement(View, {
78
+ style: resolved.navigationWrapperStyle,
79
+ pointerEvents: resolved.navigationPointerEvents,
80
+ }, renderNavigationView());
81
+ dlog(`DrawerLayoutAndroid render position=${drawerPosition ?? DEFAULT_DRAWER_POSITION} ` +
82
+ `width=${String(drawerWidth)} opened=${drawerOpened}`);
83
+ // Child order matches RN exactly: content FIRST, navigation SECOND
84
+ // (android render emits {childrenWrapper}{drawerViewWrapper}).
85
+ return createElement(resolved.viewName, {
86
+ ...resolved.hostProps,
87
+ ref: node,
88
+ onDrawerOpen: handleDrawerOpen,
89
+ onDrawerClose: handleDrawerClose,
90
+ onDrawerSlide: handleDrawerSlide,
91
+ onDrawerStateChanged: handleDrawerStateChanged,
92
+ }, contentWrapper, navigationWrapper);
93
+ });
@@ -0,0 +1,3 @@
1
+ import type { IDrawerLayoutAndroidHandle, IDrawerLayoutAndroidProps } from './shared';
2
+ export type { IDrawerPosition, IDrawerLockMode, IKeyboardDismissMode, IDrawerState, IDrawerSlideEvent, IDrawerLayoutAndroidProps, IDrawerLayoutAndroidHandle, } from './shared';
3
+ export declare const DrawerLayoutAndroid: import("react").ForwardRefExoticComponent<IDrawerLayoutAndroidProps & import("react").RefAttributes<IDrawerLayoutAndroidHandle>>;
@@ -0,0 +1,19 @@
1
+ // DrawerLayoutAndroid: base / off-Android fallback. AndroidDrawerLayout is Android-
2
+ // only, so everywhere except an Android host (where Metro picks
3
+ // drawer-layout-android.android.ts) we render the content in a plain container and drop
4
+ // the navigation view (RN's DrawerLayoutAndroidFallback shape). The imperative
5
+ // open/close are silent no-ops; there is no drawer to drive. The filename is the
6
+ // selector; no Platform.OS read. The barrel imports './drawer-layout-android', which
7
+ // resolves here under tsc/tsx and to the .android file under Metro. See ADR 0019.
8
+ import { createElement, forwardRef, useImperativeHandle } from 'react';
9
+ import { dlog } from '@symbiote/engine';
10
+ import { View } from '../../components';
11
+ export const DrawerLayoutAndroid = forwardRef((props, ref) => {
12
+ const { style, children } = props;
13
+ useImperativeHandle(ref, () => ({
14
+ openDrawer: () => dlog('DrawerLayoutAndroid.openDrawer no-op: off Android'),
15
+ closeDrawer: () => dlog('DrawerLayoutAndroid.closeDrawer no-op: off Android'),
16
+ }), []);
17
+ dlog('DrawerLayoutAndroid fallback: off-Android host, rendering content only');
18
+ return createElement(View, { style }, children);
19
+ });
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { IAccessibilityProps, IAriaProps, IDrawerLockMode, IDrawerPosition, IDrawerSlideEvent, IDrawerState, IKeyboardDismissMode } from '@symbiote/components';
3
+ import type { IStyleProp, IViewStyle } from '../../utils/styles';
4
+ export type { IDrawerPosition, IDrawerLockMode, IKeyboardDismissMode, IDrawerState, IDrawerSlideEvent, IDrawerLayoutAndroidHandle, } from '@symbiote/components';
5
+ export interface IDrawerLayoutAndroidProps extends IAccessibilityProps, IAriaProps {
6
+ drawerWidth?: number;
7
+ drawerPosition?: IDrawerPosition;
8
+ drawerLockMode?: IDrawerLockMode;
9
+ keyboardDismissMode?: IKeyboardDismissMode;
10
+ drawerBackgroundColor?: string;
11
+ statusBarBackgroundColor?: string;
12
+ onDrawerOpen?: () => void;
13
+ onDrawerClose?: () => void;
14
+ onDrawerSlide?: (event: IDrawerSlideEvent) => void;
15
+ onDrawerStateChanged?: (state: IDrawerState) => void;
16
+ renderNavigationView: () => ReactNode;
17
+ style?: IStyleProp<IViewStyle>;
18
+ children?: ReactNode;
19
+ }
@@ -0,0 +1,7 @@
1
+ // Shared types for the React DrawerLayoutAndroid: the same Props/Handle contract the real Android
2
+ // build (index.android.ts) and the off-Android fallback (index.ts) both implement. The
3
+ // platform-agnostic enums + the imperative handle live in @symbiote/components (the single source of
4
+ // truth, shared verbatim with the Vue adapter); this file adds only the React-facing prop surface and
5
+ // re-exports the agnostic types so the barrel and both builds keep importing them from one local path.
6
+ // Per ADR 0019 the filename selects the build; no Platform.OS read anywhere.
7
+ export {};
@@ -0,0 +1,73 @@
1
+ import { type ComponentType, type ReactElement, type ReactNode, type Ref } from 'react';
2
+ import { type ISymbioteEvent } from '@symbiote-native/engine';
3
+ import { type ISeparators, type ISeparatorProps, type IViewabilityConfig, type IViewabilityConfigCallbackPair, type IViewableItemsChangedInfo, type IVirtualizedListHandle } from '../virtualized-list';
4
+ import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
5
+ import type { IStyleProp, IViewStyle } from '../../utils/styles';
6
+ type IRenderItem<ItemT> = (info: {
7
+ item: ItemT;
8
+ index: number;
9
+ separators: ISeparators;
10
+ }) => ReactNode;
11
+ export type IFlatListHandle = IVirtualizedListHandle;
12
+ export interface IFlatListProps<ItemT> extends IAccessibilityProps, IAriaProps {
13
+ data: readonly ItemT[];
14
+ renderItem: IRenderItem<ItemT>;
15
+ keyExtractor?: (item: ItemT, index: number) => string;
16
+ getItemLayout?: (data: unknown, index: number) => {
17
+ length: number;
18
+ offset: number;
19
+ index: number;
20
+ };
21
+ numColumns?: number;
22
+ columnWrapperStyle?: IStyleProp<IViewStyle> | string;
23
+ ItemSeparatorComponent?: ComponentType<ISeparatorProps<ItemT>>;
24
+ ListHeaderComponent?: ComponentType<Record<string, never>> | ReactElement;
25
+ ListFooterComponent?: ComponentType<Record<string, never>> | ReactElement;
26
+ ListEmptyComponent?: ComponentType<Record<string, never>> | ReactElement;
27
+ horizontal?: boolean;
28
+ inverted?: boolean;
29
+ extraData?: unknown;
30
+ onEndReached?: (info: {
31
+ distanceFromEnd: number;
32
+ }) => void;
33
+ onEndReachedThreshold?: number;
34
+ onStartReached?: (info: {
35
+ distanceFromStart: number;
36
+ }) => void;
37
+ onStartReachedThreshold?: number;
38
+ onRefresh?: () => void;
39
+ refreshing?: boolean | null;
40
+ progressViewOffset?: number;
41
+ onViewableItemsChanged?: (info: IViewableItemsChangedInfo<ItemT>) => void;
42
+ viewabilityConfig?: IViewabilityConfig;
43
+ viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
44
+ onScrollToIndexFailed?: (info: {
45
+ index: number;
46
+ highestMeasuredFrameIndex: number;
47
+ averageItemLength: number;
48
+ }) => void;
49
+ initialNumToRender?: number;
50
+ initialScrollIndex?: number;
51
+ maxToRenderPerBatch?: number;
52
+ updateCellsBatchingPeriod?: number;
53
+ windowSize?: number;
54
+ maintainVisibleContentPosition?: {
55
+ minIndexForVisible: number;
56
+ autoscrollToTopThreshold?: number;
57
+ };
58
+ onScroll?: (event: ISymbioteEvent) => void;
59
+ onScrollBeginDrag?: (event: ISymbioteEvent) => void;
60
+ onScrollEndDrag?: (event: ISymbioteEvent) => void;
61
+ onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
62
+ onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
63
+ scrollEventThrottle?: number;
64
+ keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
65
+ keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
66
+ style?: IStyleProp<IViewStyle>;
67
+ contentContainerStyle?: IStyleProp<IViewStyle>;
68
+ className?: string;
69
+ }
70
+ export declare function FlatList<ItemT>(props: IFlatListProps<ItemT> & {
71
+ ref?: Ref<IFlatListHandle>;
72
+ }): ReactElement;
73
+ export {};
@@ -0,0 +1,86 @@
1
+ // FlatList: the convenience surface over VirtualizedList. It takes a plain `data` array and
2
+ // derives getItem/getItemCount, so callers never touch the VirtualizedList data-access protocol.
3
+ // `numColumns` packs that many items into each row (a horizontal sub-View), so the virtualized
4
+ // stream is rows, not items (RN's FlatList). All windowing, viewability, batching, and imperative
5
+ // scrolling are inherited from VirtualizedList; the data shaping and the row/viewability/separator
6
+ // transforms are shared from @symbiote-native/components. This file only adapts to React's lifecycle
7
+ // (element creation + ref threading).
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';
11
+ import { VirtualizedList, } from '../virtualized-list';
12
+ export function FlatList(props) {
13
+ const { ref, data, renderItem, keyExtractor, numColumns = SINGLE_COLUMN, columnWrapperStyle,
14
+ // onViewableItemsChanged/viewability are typed against ItemT here; in the multi-column path
15
+ // the underlying stream is IRow<ItemT>, so they are dropped from `rest` and pulled out.
16
+ onViewableItemsChanged, viewabilityConfigCallbackPairs,
17
+ // ItemSeparatorComponent is typed on ItemT; the multi-column stream is IRow<ItemT>, so it is
18
+ // wrapped there to unwrap rows back to items, exactly like viewability above.
19
+ ItemSeparatorComponent, ...rest } = props;
20
+ dlog(`FlatList over ${data.length} items, ${numColumns} column(s)`);
21
+ if (numColumns <= SINGLE_COLUMN) {
22
+ return createElement((VirtualizedList), {
23
+ ref,
24
+ data,
25
+ getItem: (_source, index) => data[index],
26
+ getItemCount: () => data.length,
27
+ renderItem,
28
+ keyExtractor,
29
+ onViewableItemsChanged,
30
+ viewabilityConfigCallbackPairs,
31
+ ItemSeparatorComponent,
32
+ ...rest,
33
+ });
34
+ }
35
+ // Multi-column: the virtualized stream is rows. Each cell renders its items side by side in a
36
+ // flex-row View so windowing accounts for whole rows.
37
+ const rows = chunkIntoRows(data, numColumns);
38
+ const resolvedColumnWrapperStyle = typeof columnWrapperStyle === 'string'
39
+ ? resolveClassName(columnWrapperStyle)
40
+ : columnWrapperStyle;
41
+ const rowStyle = [{ flexDirection: 'row' }, resolvedColumnWrapperStyle];
42
+ const renderRow = (info) => {
43
+ const cells = info.item.items.map((item, column) => {
44
+ const index = info.item.startIndex + column;
45
+ const key = keyExtractor ? keyExtractor(item, index) : String(index);
46
+ // The row IS the virtualized cell, so every item in it shares the row's separators handle
47
+ // (the divider sits between rows, not columns), like RN's multi-column FlatList.
48
+ return createElement('symbiote-view', { key, style: { flex: 1 } }, renderItem({ item, index, separators: info.separators }));
49
+ });
50
+ return createElement('symbiote-view', { style: rowStyle }, ...cells);
51
+ };
52
+ // Viewability over rows expands back to per-item tokens so the caller sees item-level
53
+ // visibility, not row-level (shared expandRowViewability).
54
+ const rowOnViewableItemsChanged = onViewableItemsChanged !== undefined
55
+ ? (rowInfo) => {
56
+ onViewableItemsChanged(expandRowViewability(rowInfo, keyExtractor));
57
+ }
58
+ : undefined;
59
+ const rowViewabilityPairs = viewabilityConfigCallbackPairs?.map(pair => ({
60
+ viewabilityConfig: pair.viewabilityConfig,
61
+ onViewableItemsChanged: (rowInfo) => {
62
+ pair.onViewableItemsChanged(expandRowViewability(rowInfo, keyExtractor));
63
+ },
64
+ }));
65
+ // The divider between rows shows real items (last of the row above, first of the row below), so
66
+ // the user's separator, typed on ItemT, sees items rather than the IRow wrapper.
67
+ const rowSeparatorComponent = ItemSeparatorComponent === undefined
68
+ ? undefined
69
+ : (rowProps) => createElement(ItemSeparatorComponent, {
70
+ ...rowProps,
71
+ leadingItem: lastItemOfRow(rowProps.leadingItem),
72
+ trailingItem: firstItemOfRow(rowProps.trailingItem),
73
+ });
74
+ return createElement((VirtualizedList), {
75
+ ref,
76
+ data: rows,
77
+ getItem: (_source, index) => rows[index],
78
+ getItemCount: () => rows.length,
79
+ renderItem: renderRow,
80
+ keyExtractor: rowKeyExtractor,
81
+ onViewableItemsChanged: rowOnViewableItemsChanged,
82
+ viewabilityConfigCallbackPairs: rowViewabilityPairs,
83
+ ItemSeparatorComponent: rowSeparatorComponent,
84
+ ...rest,
85
+ });
86
+ }
@@ -0,0 +1,9 @@
1
+ import { type FC } from 'react';
2
+ import { type IImageStatics, type IImageProps as IImageBaseProps } from '@symbiote-native/components';
3
+ export type IImageProps = IImageBaseProps & {
4
+ className?: string;
5
+ };
6
+ export type IImageWithStatics = FC<IImageProps> & IImageStatics;
7
+ export declare const Image: IImageWithStatics;
8
+ export { setImageSourceResolver } from '@symbiote-native/components';
9
+ export type { IImageSource, IImageSourceProp, IResizeMode, IImageSize, IImageCacheStatus, } from '@symbiote-native/components';