@symbiote-native/engine 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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/build/accessibility-info/index.android.d.ts +3 -0
  4. package/build/accessibility-info/index.android.js +166 -0
  5. package/build/accessibility-info/index.d.ts +1 -0
  6. package/build/accessibility-info/index.ios.d.ts +3 -0
  7. package/build/accessibility-info/index.ios.js +219 -0
  8. package/build/accessibility-info/index.js +5 -0
  9. package/build/accessibility-info/shared.d.ts +34 -0
  10. package/build/accessibility-info/shared.js +13 -0
  11. package/build/action-sheet-ios/index.d.ts +36 -0
  12. package/build/action-sheet-ios/index.js +74 -0
  13. package/build/alert/index.android.d.ts +5 -0
  14. package/build/alert/index.android.js +117 -0
  15. package/build/alert/index.d.ts +1 -0
  16. package/build/alert/index.ios.d.ts +7 -0
  17. package/build/alert/index.ios.js +83 -0
  18. package/build/alert/index.js +8 -0
  19. package/build/alert/shared.d.ts +19 -0
  20. package/build/alert/shared.js +17 -0
  21. package/build/animated/animated-component-shared.d.ts +5 -0
  22. package/build/animated/animated-component-shared.js +54 -0
  23. package/build/animated/animation.d.ts +9 -0
  24. package/build/animated/animation.js +6 -0
  25. package/build/animated/animations/base.d.ts +27 -0
  26. package/build/animated/animations/base.js +90 -0
  27. package/build/animated/animations/composition.d.ts +38 -0
  28. package/build/animated/animations/composition.js +236 -0
  29. package/build/animated/animations/decay.d.ts +22 -0
  30. package/build/animated/animations/decay.js +65 -0
  31. package/build/animated/animations/raf.d.ts +5 -0
  32. package/build/animated/animations/raf.js +39 -0
  33. package/build/animated/animations/spring-config.d.ts +6 -0
  34. package/build/animated/animations/spring-config.js +55 -0
  35. package/build/animated/animations/spring.d.ts +50 -0
  36. package/build/animated/animations/spring.js +207 -0
  37. package/build/animated/animations/timing.d.ts +27 -0
  38. package/build/animated/animations/timing.js +101 -0
  39. package/build/animated/animations/tracking.d.ts +14 -0
  40. package/build/animated/animations/tracking.js +43 -0
  41. package/build/animated/bezier.d.ts +1 -0
  42. package/build/animated/bezier.js +101 -0
  43. package/build/animated/color.d.ts +37 -0
  44. package/build/animated/color.js +183 -0
  45. package/build/animated/easing.d.ts +20 -0
  46. package/build/animated/easing.js +96 -0
  47. package/build/animated/event.d.ts +36 -0
  48. package/build/animated/event.js +252 -0
  49. package/build/animated/graph.d.ts +38 -0
  50. package/build/animated/graph.js +227 -0
  51. package/build/animated/index.d.ts +20 -0
  52. package/build/animated/index.js +28 -0
  53. package/build/animated/interpolation-node.d.ts +16 -0
  54. package/build/animated/interpolation-node.js +57 -0
  55. package/build/animated/interpolation.d.ts +22 -0
  56. package/build/animated/interpolation.js +199 -0
  57. package/build/animated/mock.d.ts +56 -0
  58. package/build/animated/mock.js +127 -0
  59. package/build/animated/native/native-animated.d.ts +43 -0
  60. package/build/animated/native/native-animated.js +146 -0
  61. package/build/animated/operators.d.ts +80 -0
  62. package/build/animated/operators.js +266 -0
  63. package/build/animated/props.d.ts +20 -0
  64. package/build/animated/props.js +187 -0
  65. package/build/animated/style.d.ts +26 -0
  66. package/build/animated/style.js +187 -0
  67. package/build/animated/value-xy.d.ts +35 -0
  68. package/build/animated/value-xy.js +106 -0
  69. package/build/animated/value.d.ts +36 -0
  70. package/build/animated/value.js +185 -0
  71. package/build/app-registry/index.d.ts +40 -0
  72. package/build/app-registry/index.js +144 -0
  73. package/build/app-state/index.d.ts +16 -0
  74. package/build/app-state/index.js +105 -0
  75. package/build/appearance/index.d.ts +12 -0
  76. package/build/appearance/index.js +84 -0
  77. package/build/back-handler/index.d.ts +14 -0
  78. package/build/back-handler/index.js +106 -0
  79. package/build/commit.d.ts +16 -0
  80. package/build/commit.js +678 -0
  81. package/build/debug.d.ts +5 -0
  82. package/build/debug.js +18 -0
  83. package/build/dimensions/index.d.ts +28 -0
  84. package/build/dimensions/index.js +148 -0
  85. package/build/dispatch.d.ts +2 -0
  86. package/build/dispatch.js +18 -0
  87. package/build/events/index.d.ts +1 -0
  88. package/build/events/index.js +691 -0
  89. package/build/fabric.d.ts +32 -0
  90. package/build/fabric.js +59 -0
  91. package/build/host-instance/index.d.ts +11 -0
  92. package/build/host-instance/index.js +49 -0
  93. package/build/i18n-manager/index.d.ts +13 -0
  94. package/build/i18n-manager/index.js +91 -0
  95. package/build/index.d.ts +80 -0
  96. package/build/index.js +72 -0
  97. package/build/interaction-manager/index.d.ts +45 -0
  98. package/build/interaction-manager/index.js +222 -0
  99. package/build/keyboard/index.d.ts +31 -0
  100. package/build/keyboard/index.js +142 -0
  101. package/build/layout-animation/index.d.ts +66 -0
  102. package/build/layout-animation/index.js +183 -0
  103. package/build/linking/index.android.d.ts +2 -0
  104. package/build/linking/index.android.js +18 -0
  105. package/build/linking/index.d.ts +1 -0
  106. package/build/linking/index.ios.d.ts +2 -0
  107. package/build/linking/index.ios.js +9 -0
  108. package/build/linking/index.js +6 -0
  109. package/build/linking/shared.d.ts +32 -0
  110. package/build/linking/shared.js +98 -0
  111. package/build/native-events.d.ts +24 -0
  112. package/build/native-events.js +129 -0
  113. package/build/native-modules.d.ts +6 -0
  114. package/build/native-modules.js +57 -0
  115. package/build/node.d.ts +36 -0
  116. package/build/node.js +194 -0
  117. package/build/pan-responder/index.d.ts +53 -0
  118. package/build/pan-responder/index.js +353 -0
  119. package/build/permissions-android/index.d.ts +115 -0
  120. package/build/permissions-android/index.js +185 -0
  121. package/build/pixel-ratio/index.d.ts +8 -0
  122. package/build/pixel-ratio/index.js +27 -0
  123. package/build/platform/index.android.d.ts +22 -0
  124. package/build/platform/index.android.js +60 -0
  125. package/build/platform/index.d.ts +1 -0
  126. package/build/platform/index.ios.d.ts +18 -0
  127. package/build/platform/index.ios.js +62 -0
  128. package/build/platform/index.js +5 -0
  129. package/build/platform/shared.d.ts +25 -0
  130. package/build/platform/shared.js +41 -0
  131. package/build/platform-color.d.ts +19 -0
  132. package/build/platform-color.js +25 -0
  133. package/build/post-commit.d.ts +4 -0
  134. package/build/post-commit.js +16 -0
  135. package/build/process-aspect-ratio.d.ts +1 -0
  136. package/build/process-aspect-ratio.js +34 -0
  137. package/build/process-background-image/index.d.ts +28 -0
  138. package/build/process-background-image/index.js +557 -0
  139. package/build/process-box-shadow/index.d.ts +11 -0
  140. package/build/process-box-shadow/index.js +193 -0
  141. package/build/process-filter.d.ts +31 -0
  142. package/build/process-filter.js +304 -0
  143. package/build/process-font-variant.d.ts +1 -0
  144. package/build/process-font-variant.js +17 -0
  145. package/build/process-transform/index.d.ts +5 -0
  146. package/build/process-transform/index.js +120 -0
  147. package/build/process-transform-origin/index.d.ts +3 -0
  148. package/build/process-transform-origin/index.js +108 -0
  149. package/build/registry.d.ts +31 -0
  150. package/build/registry.js +145 -0
  151. package/build/settings/index.d.ts +8 -0
  152. package/build/settings/index.js +126 -0
  153. package/build/share/index.android.d.ts +3 -0
  154. package/build/share/index.android.js +56 -0
  155. package/build/share/index.d.ts +1 -0
  156. package/build/share/index.ios.d.ts +3 -0
  157. package/build/share/index.ios.js +47 -0
  158. package/build/share/index.js +6 -0
  159. package/build/share/shared.d.ts +32 -0
  160. package/build/share/shared.js +32 -0
  161. package/build/status-bar/index.android.d.ts +5 -0
  162. package/build/status-bar/index.android.js +83 -0
  163. package/build/status-bar/index.d.ts +1 -0
  164. package/build/status-bar/index.ios.d.ts +5 -0
  165. package/build/status-bar/index.ios.js +66 -0
  166. package/build/status-bar/index.js +4 -0
  167. package/build/status-bar/shared.d.ts +22 -0
  168. package/build/status-bar/shared.js +22 -0
  169. package/build/style/index.d.ts +1 -0
  170. package/build/style/index.js +30 -0
  171. package/build/style-registry/index.d.ts +11 -0
  172. package/build/style-registry/index.js +165 -0
  173. package/build/style-sheet/index.d.ts +20 -0
  174. package/build/style-sheet/index.js +121 -0
  175. package/build/styles.d.ts +220 -0
  176. package/build/styles.js +7 -0
  177. package/build/surface.d.ts +16 -0
  178. package/build/surface.js +67 -0
  179. package/build/tags.d.ts +1 -0
  180. package/build/tags.js +10 -0
  181. package/build/text-input-state.d.ts +5 -0
  182. package/build/text-input-state.js +29 -0
  183. package/build/toast-android/index.d.ts +10 -0
  184. package/build/toast-android/index.js +108 -0
  185. package/build/vibration/index.android.d.ts +2 -0
  186. package/build/vibration/index.android.js +18 -0
  187. package/build/vibration/index.d.ts +1 -0
  188. package/build/vibration/index.ios.d.ts +2 -0
  189. package/build/vibration/index.ios.js +54 -0
  190. package/build/vibration/index.js +6 -0
  191. package/build/vibration/shared.d.ts +15 -0
  192. package/build/vibration/shared.js +68 -0
  193. package/build/view-config.d.ts +1 -0
  194. package/build/view-config.js +114 -0
  195. package/package.json +41 -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,124 @@
1
+ # @symbiote-native/engine
2
+
3
+ The **retained shadow-tree engine** at the bottom of [SymbioteJS](../../README.md) — the one
4
+ package every framework adapter (`@symbiote-native/react`, `@symbiote-native/vue`, `@symbiote-native/angular`, …)
5
+ drives, and the only place the mutation→clone-on-write translation into React Native's Fabric
6
+ exists. It holds a retained, mutable tree of nodes that an adapter mutates cheaply
7
+ (`appendChild` / `setProp` / `removeChild` …), then on commit diffs that tree against Fabric's
8
+ current one, clones only what changed, and calls `completeRoot` — the persistent, clone-on-write
9
+ dance Fabric requires, done **once**, for every framework.
10
+
11
+ > New to SymbioteJS? The [root README](../../README.md) has the architecture and the one fact it
12
+ > rests on — React is just *one client* of `nativeFabricUIManager`. This package is what sits
13
+ > between every adapter and that native slot.
14
+
15
+ ---
16
+
17
+ ## Who calls this, directly vs. indirectly
18
+
19
+ **Most consumers never import this package by name.** An app written against
20
+ `@symbiote-native/react`/`@symbiote-native/vue`/`@symbiote-native/angular` never calls `createElement` or
21
+ `setProp` itself — the adapter's reconciler does that on the app's behalf. You reach for
22
+ `@symbiote-native/engine` directly only when:
23
+
24
+ - you are **writing or debugging a framework adapter** (a `react-reconciler` host config, a Vue
25
+ `createRenderer`, an Angular `Renderer2`) — this is its primary audience;
26
+ - you need one of the **framework-agnostic runtime modules** it re-exports (`Platform`,
27
+ `StyleSheet`, `Dimensions`, `Alert`, `Animated`, …) — every adapter re-exports these verbatim, so
28
+ most apps still reach them through `@symbiote-native/react` etc., not this package.
29
+
30
+ The mutation API below is intentionally low-level and closely mirrors Fabric's own persistent
31
+ semantics — it is an internal seam, not an app-facing API.
32
+
33
+ ---
34
+
35
+ ## The mutation API — `core/engine/src/node.ts`
36
+
37
+ The entire surface a renderer seam drives:
38
+
39
+ ```ts
40
+ import {
41
+ createElement, createRawText, createAnchor,
42
+ appendChild, insertBefore, removeChild,
43
+ routeProp, setEventListener, setProp, setText,
44
+ } from '@symbiote-native/engine';
45
+
46
+ const node = createElement('RCTView'); // component IS the Fabric view name
47
+ const text = createRawText('Hello');
48
+ appendChild(node, text);
49
+ routeProp(node, 'onPress', () => {}); // ← the flat-bag entry point (React/Vue/Solid):
50
+ // decides event-vs-prop via the ViewConfig,
51
+ // NOT by the "onX" naming convention
52
+ ```
53
+
54
+ `routeProp` is the one call a flat-bag adapter should route every prop through — a **structural**
55
+ adapter (Angular's `Renderer2.listen`, Svelte's `addEventListener`) already knows the event name
56
+ and calls `setEventListener` directly instead.
57
+
58
+ ### Committing — `SymbioteSurface`
59
+
60
+ ```ts
61
+ import { createSurface } from '@symbiote-native/engine';
62
+
63
+ const surface = createSurface(rootTag);
64
+ surface.appendChild(root, node);
65
+ surface.commit(); // synchronous — for a framework that already batches (React)
66
+ // surface.requestCommit(); // microtask-coalesced — for reactive frameworks (Vue/Svelte/Angular)
67
+ ```
68
+
69
+ Every imperative call in the bridge below (`dispatchViewCommand`, `measure`, `setNativeProps`, …)
70
+ is gated on the node having actually committed — see `whenCommitted` for wiring a native call
71
+ before a tag is guaranteed to exist.
72
+
73
+ ---
74
+
75
+ ## What else it exports
76
+
77
+ - **The imperative/native bridge** — `dispatchViewCommand`, `measure` / `measureInWindow` /
78
+ `measureLayout`, `getNativeTag`, `getNativeNode`, `setNativeProps`, `sendAccessibilityEvent`,
79
+ `whenCommitted`, `toPublicInstance` (the `ref` handle every adapter grafts onto a host node).
80
+ - **Runtime modules**, framework-agnostic, re-exported by every adapter: `Platform`,
81
+ `StyleSheet` (+ `computeHairlineWidth`), `Dimensions`, `PixelRatio`, `Appearance`, `AppState`,
82
+ `Keyboard`, `AccessibilityInfo`, `BackHandler`, `PermissionsAndroid`, `LayoutAnimation`,
83
+ `InteractionManager`, `PanResponder`, and the imperative modules `Alert`, `Share`,
84
+ `ActionSheetIOS`, `Linking`, `Vibration`, `ToastAndroid`, `Settings`, `I18nManager`.
85
+ - **`Animated`** — both the JS and native driver (`timing` / `spring` / `decay` / `loop` /
86
+ `ValueXY` / tracking / `diffClamp` / `Easing`), including the native-event attachment path
87
+ (`attachNativeEvent`, `AnimatedEvent`).
88
+ - **The style pipeline** — `flattenStyle`, the CSS-style processors RN itself runs in JS
89
+ (`processBoxShadow`, `processFilter`, `processTransform`, `processTransformOrigin`,
90
+ `processAspectRatio`, `processFontVariant`, `processBackgroundImage`), and the runtime
91
+ **class-name registry** (`registerStyles` / `resolveClassName` / `scopeClassName`) that
92
+ `@symbiote-native/css-parser`'s build-time output resolves against — shared by every adapter's
93
+ `class` / `className` / `addClass` prop path.
94
+ - **`AppRegistry` core** (`createAppRegistry`) — registry bookkeeping + headless-task plumbing;
95
+ each adapter supplies only its own `runnableFor`.
96
+ - **`dlog` / `isDebug`** — the diagnostic-logging seam every adapter and this package route
97
+ through, gated by the `DEBUG` env var, never a bare `console.log`.
98
+
99
+ ## What it does NOT do
100
+
101
+ - It does not know about React, Vue, Angular, JSX, templates, or reactivity — an adapter maps its
102
+ own framework idioms onto this API, never the other way around.
103
+ - It does not touch Fabric C++, JSI, or Yoga directly — it calls `nativeFabricUIManager`
104
+ (`createNode` / `cloneNodeWithNewProps` / `appendChildToSet` / `completeRoot`), the same
105
+ framework-agnostic seam React's own renderer uses.
106
+ - It is not a component library — visual components (Switch, Modal, the lists, …) live in
107
+ [`@symbiote-native/components`](../components), built on top of this package's `Descriptor`-free
108
+ mutation API.
109
+
110
+ ## Related packages
111
+
112
+ - [`@symbiote-native/components`](../components) — the framework-agnostic component layer (state +
113
+ render), built on this engine.
114
+ - [`@symbiote-native/react`](../../adapters/react) / [`@symbiote-native/vue`](../../adapters/vue) /
115
+ [`@symbiote-native/angular`](../../adapters/angular) — the framework adapters that drive this API.
116
+ - [`@symbiote-native/css-parser`](../css-parser) — compiles CSS into the style objects this package's
117
+ `style-registry` resolves at runtime.
118
+
119
+ ## Test it
120
+
121
+ ```bash
122
+ pnpm test # vitest, from the workspace root — headless, against a fake Fabric slot
123
+ DEBUG=1 pnpm test # same, with diagnostic logs on
124
+ ```
@@ -0,0 +1,3 @@
1
+ import { type IAccessibilityInfoStatic } from './shared';
2
+ export type { IAccessibilityChangeEvent, IAccessibilityChangeEventName, IAccessibilityChangeEventHandler, IAccessibilityAnnouncementFinishedEvent, IAnnounceForAccessibilityOptions, IAccessibilityEventType, } from './shared';
3
+ export declare const AccessibilityInfo: IAccessibilityInfoStatic;
@@ -0,0 +1,166 @@
1
+ // AccessibilityInfo on Android wraps the stock RN `AccessibilityInfo` native module
2
+ // (NO native code added; it ships with react-native). Android's getters take a SINGLE
3
+ // success callback (no error callback) and a different method set than iOS: screen-reader
4
+ // is `isTouchExplorationEnabled`, plus reduce-motion / invert-colors / grayscale /
5
+ // high-text-contrast / accessibility-service, and `getRecommendedTimeoutMillis`. The
6
+ // device-event NAMES also differ from iOS (e.g. screen-reader is `touchExplorationDidChange`,
7
+ // reduce-motion is `reduceMotionDidChange`). Metro picks this on an Android host. Mirrors
8
+ // RN's AccessibilityInfo.js Android branches.
9
+ import { getNativeModule } from '../native-modules';
10
+ import { installDeviceEventHub, NativeEventEmitter, } from '../native-events';
11
+ import { isSymbioteNode } from '../node';
12
+ import { sendAccessibilityEvent as sharedSendAccessibilityEvent } from '../commit';
13
+ import { dlog } from '../debug';
14
+ import { isBoolean, } from './shared';
15
+ // The Android native module name. This is the module the Android JS wrapper
16
+ // (INativeAccessibilityInfoAndroid) resolves: the stock RN `AccessibilityInfo` Turbo/legacy
17
+ // module. Per the symbiote invariant, a module name is only provable on a real host (a
18
+ // headless fake answers to any name); this Android name is DEVICE-VERIFY-PENDING. See
19
+ // .docs/native-module-platform-routing.md.
20
+ const ACCESSIBILITY_MODULE = 'AccessibilityInfo';
21
+ // Public event name -> the Android device event the native side emits. Android renames
22
+ // most of them; events with no Android source (iOS-only) are absent and yield an inert
23
+ // subscription. (RN maps both `change` and `screenReaderChanged` to touchExplorationDidChange.)
24
+ const ANDROID_DEVICE_EVENT = {
25
+ screenReaderChanged: 'touchExplorationDidChange',
26
+ reduceMotionChanged: 'reduceMotionDidChange',
27
+ highTextContrastChanged: 'highTextContrastDidChange',
28
+ accessibilityServiceChanged: 'accessibilityServiceDidChange',
29
+ invertColorsChanged: 'invertColorDidChange',
30
+ grayscaleChanged: 'grayscaleModeDidChange',
31
+ };
32
+ // Lazily resolved so importing this module has no native side effect. `null` when unlinked.
33
+ let accessibilityModule;
34
+ let emitter;
35
+ function getModule() {
36
+ if (accessibilityModule === undefined) {
37
+ accessibilityModule = getNativeModule(ACCESSIBILITY_MODULE);
38
+ dlog(`AccessibilityInfo(android): module ${accessibilityModule ? 'resolved' : 'NOT resolved (null)'}`);
39
+ }
40
+ return accessibilityModule;
41
+ }
42
+ function getEmitter() {
43
+ if (emitter === undefined) {
44
+ installDeviceEventHub();
45
+ emitter = new NativeEventEmitter(getModule() ?? undefined);
46
+ }
47
+ return emitter;
48
+ }
49
+ // Run a single-callback Android getter as a Promise. Resolves false when the module is
50
+ // unlinked OR the optional method is absent on this host; mirrors RN's "missing query ->
51
+ // false" contract for the cross-platform getters. The dlog records the miss.
52
+ function queryState(pick, label) {
53
+ const module = getModule();
54
+ if (module === null) {
55
+ dlog(`AccessibilityInfo(android).${label} -> no module (false)`);
56
+ return Promise.resolve(false);
57
+ }
58
+ const getter = pick(module);
59
+ if (getter === undefined) {
60
+ dlog(`AccessibilityInfo(android).${label} -> method absent (false)`);
61
+ return Promise.resolve(false);
62
+ }
63
+ return new Promise(resolve => {
64
+ getter.call(module, enabled => resolve(enabled));
65
+ });
66
+ }
67
+ class AccessibilityInfoAndroid {
68
+ // Screen reader on Android == touch exploration (TalkBack).
69
+ isScreenReaderEnabled() {
70
+ return queryState(m => m.isTouchExplorationEnabled, 'isScreenReaderEnabled');
71
+ }
72
+ isReduceMotionEnabled() {
73
+ return queryState(m => m.isReduceMotionEnabled, 'isReduceMotionEnabled');
74
+ }
75
+ // iOS-only query; Android has no bold-text setting, so resolve false (RN parity).
76
+ isBoldTextEnabled() {
77
+ return Promise.resolve(false);
78
+ }
79
+ isGrayscaleEnabled() {
80
+ return queryState(m => m.isGrayscaleEnabled, 'isGrayscaleEnabled');
81
+ }
82
+ isInvertColorsEnabled() {
83
+ return queryState(m => m.isInvertColorsEnabled, 'isInvertColorsEnabled');
84
+ }
85
+ // iOS-only query; resolve false (RN parity).
86
+ isReduceTransparencyEnabled() {
87
+ return Promise.resolve(false);
88
+ }
89
+ isHighTextContrastEnabled() {
90
+ return queryState(m => m.isHighTextContrastEnabled, 'isHighTextContrastEnabled');
91
+ }
92
+ // iOS-only "Increase Contrast"; Android has no equivalent, so resolve false (RN parity).
93
+ isDarkerSystemColorsEnabled() {
94
+ return Promise.resolve(false);
95
+ }
96
+ // iOS-only reduce-motion sub-setting; resolve false on Android (RN parity).
97
+ prefersCrossFadeTransitions() {
98
+ return Promise.resolve(false);
99
+ }
100
+ isAccessibilityServiceEnabled() {
101
+ return queryState(m => m.isAccessibilityServiceEnabled, 'isAccessibilityServiceEnabled');
102
+ }
103
+ // Post a string to be announced by the screen reader. No-op without a module.
104
+ announceForAccessibility(announcement) {
105
+ const module = getModule();
106
+ if (module === null) {
107
+ dlog('AccessibilityInfo(android).announceForAccessibility -> no module (no-op)');
108
+ return;
109
+ }
110
+ module.announceForAccessibility(announcement);
111
+ }
112
+ // Android ignores queue/priority options (iOS-only) and posts the announcement plainly
113
+ // (RN parity).
114
+ announceForAccessibilityWithOptions(announcement, _options) {
115
+ this.announceForAccessibility(announcement);
116
+ }
117
+ // Deprecated focus-by-tag entry. The Fabric slot keys on a node's committed handle, and a
118
+ // bare reactTag can't be resolved back to its SymbioteNode (the mirror is node-keyed), so
119
+ // this best-effort path is a logged no-op. Callers should use sendAccessibilityEvent(node,
120
+ // 'focus') with a host ref, which routes a real node through the slot.
121
+ setAccessibilityFocus(reactTag) {
122
+ dlog(`AccessibilityInfo(android).setAccessibilityFocus(${reactTag}) -> tag-only, no node to route (no-op)`);
123
+ }
124
+ // Recommended UI-change timeout for this user. Resolves the original when the module or
125
+ // the query is absent (RN parity).
126
+ getRecommendedTimeoutMillis(originalTimeout) {
127
+ const module = getModule();
128
+ if (module === null || module.getRecommendedTimeoutMillis === undefined) {
129
+ return Promise.resolve(originalTimeout);
130
+ }
131
+ const query = module.getRecommendedTimeoutMillis;
132
+ return new Promise(resolve => {
133
+ query.call(module, originalTimeout, timeout => resolve(timeout));
134
+ });
135
+ }
136
+ // Emit a named accessibility event at a view through the Fabric slot. RN's Fabric path
137
+ // hands the public-instance handle to nativeFabricUIManager.sendAccessibilityEvent with the
138
+ // STRING eventType; the C++ side maps it to the platform's AccessibilityEvent kind. The
139
+ // handle here IS the SymbioteNode (symbiote augments the node in place as its public
140
+ // instance), so resolve it with the runtime guard and route through shared.
141
+ sendAccessibilityEvent(handle, eventType) {
142
+ if (!isSymbioteNode(handle)) {
143
+ dlog(`AccessibilityInfo(android).sendAccessibilityEvent("${eventType}") -> handle is not a node (no-op)`);
144
+ return;
145
+ }
146
+ dlog(`AccessibilityInfo(android).sendAccessibilityEvent("${eventType}") -> slot`);
147
+ sharedSendAccessibilityEvent(handle, eventType);
148
+ }
149
+ // Subscribe to an accessibility-state change. Android events all carry a bare boolean.
150
+ // Never throws: a public event with no Android device mapping yields an inert
151
+ // subscription; a missing module yields a live-but-silent one.
152
+ addEventListener(eventName, handler) {
153
+ const deviceEvent = ANDROID_DEVICE_EVENT[eventName];
154
+ dlog(`AccessibilityInfo(android).addEventListener -> ${eventName} (device: ${deviceEvent ?? 'none'})`);
155
+ if (deviceEvent === undefined) {
156
+ return { remove() { } };
157
+ }
158
+ const eventEmitter = getEmitter();
159
+ return eventEmitter.addListener(deviceEvent, payload => {
160
+ if (!isBoolean(payload))
161
+ return;
162
+ handler(payload);
163
+ });
164
+ }
165
+ }
166
+ export const AccessibilityInfo = new AccessibilityInfoAndroid();
@@ -0,0 +1 @@
1
+ export * from './index.ios';
@@ -0,0 +1,3 @@
1
+ import { type IAccessibilityInfoStatic } from './shared';
2
+ export type { IAccessibilityChangeEvent, IAccessibilityChangeEventName, IAccessibilityChangeEventHandler, IAccessibilityAnnouncementFinishedEvent, IAnnounceForAccessibilityOptions, IAccessibilityEventType, } from './shared';
3
+ export declare const AccessibilityInfo: IAccessibilityInfoStatic;
@@ -0,0 +1,219 @@
1
+ // AccessibilityInfo on iOS wraps the `AccessibilityManager` native module: callback-
2
+ // based state getters (VoiceOver / reduce-motion / bold-text / grayscale / invert-colors /
3
+ // reduce-transparency / darker-system-colors), announce + focus side effects, and the
4
+ // observe-counters for the device-event subscription. Subscribes to iOS device events
5
+ // (`screenReaderChanged` / `reduceMotionChanged` / `boldTextChanged` / …) via a
6
+ // NativeEventEmitter and re-broadcasts to JS listeners. Metro picks this on an iOS host;
7
+ // the bare accessibility-info.ts re-exports it as the default for tsc / tsx / headless.
8
+ // Mirrors RN's AccessibilityInfo.js iOS branches.
9
+ import { getNativeModule } from '../native-modules';
10
+ import { installDeviceEventHub, NativeEventEmitter, } from '../native-events';
11
+ import { isSymbioteNode } from '../node';
12
+ import { sendAccessibilityEvent as sharedSendAccessibilityEvent } from '../commit';
13
+ import { dlog } from '../debug';
14
+ import { isBoolean, } from './shared';
15
+ // The iOS native module name RN registers this under. NOTE: this is the name the iOS JS
16
+ // wrapper (INativeAccessibilityManagerIOS) resolves via
17
+ // `TurboModuleRegistry.get('AccessibilityManager')`, NOT the spec filename
18
+ // `NativeAccessibilityManager`. Per the symbiote invariant, a module name is only provable
19
+ // on a real host (a headless fake answers to any name); this iOS name is device-verified
20
+ // (the pre-split file shipped it). See .docs/native-module-platform-routing.md.
21
+ const ACCESSIBILITY_MODULE = 'AccessibilityManager';
22
+ // Public event name -> the iOS device event the native side emits. iOS keeps the names
23
+ // 1:1; the indirection exists only so the mapping stays explicit (Android renames them).
24
+ const IOS_DEVICE_EVENT = {
25
+ screenReaderChanged: 'screenReaderChanged',
26
+ reduceMotionChanged: 'reduceMotionChanged',
27
+ boldTextChanged: 'boldTextChanged',
28
+ grayscaleChanged: 'grayscaleChanged',
29
+ invertColorsChanged: 'invertColorsChanged',
30
+ reduceTransparencyChanged: 'reduceTransparencyChanged',
31
+ darkerSystemColorsChanged: 'darkerSystemColorsChanged',
32
+ announcementFinished: 'announcementFinished',
33
+ };
34
+ // Lazily resolved so importing this module has no native side effect: a headless run
35
+ // without a fake __turboModuleProxy still loads it; resolution happens on first use.
36
+ // `null` when the module isn't linked.
37
+ let accessibilityModule;
38
+ let emitter;
39
+ function getModule() {
40
+ if (accessibilityModule === undefined) {
41
+ accessibilityModule = getNativeModule(ACCESSIBILITY_MODULE);
42
+ dlog(`AccessibilityInfo(ios): module ${accessibilityModule ? 'resolved' : 'NOT resolved (null)'}`);
43
+ }
44
+ return accessibilityModule;
45
+ }
46
+ function getEmitter() {
47
+ if (emitter === undefined) {
48
+ // WHY lazy: install on first subscribe so the hub exists before native emits,
49
+ // without a hard bootstrap-order dependency. Idempotent.
50
+ installDeviceEventHub();
51
+ emitter = new NativeEventEmitter(getModule() ?? undefined);
52
+ }
53
+ return emitter;
54
+ }
55
+ // Run a callback-based native getter as a Promise; resolves false when the module is
56
+ // unlinked, mirroring RN's "unavailable query -> false" contract for the cross-platform
57
+ // getters. (RN rejects on iOS, but a false fallback keeps the unified surface uniform with
58
+ // Android's missing-method getters; the dlog records the miss.)
59
+ function queryState(pick, label) {
60
+ const module = getModule();
61
+ if (module === null) {
62
+ dlog(`AccessibilityInfo(ios).${label} -> no module (false)`);
63
+ return Promise.resolve(false);
64
+ }
65
+ const getter = pick(module);
66
+ return new Promise((resolve, reject) => {
67
+ getter.call(module, enabled => resolve(enabled), error => reject(error));
68
+ });
69
+ }
70
+ // Like queryState, but for an OPTIONAL native getter (newer iOS surfaces): resolves false
71
+ // when the module is unlinked OR the method is absent on this host, instead of throwing.
72
+ function queryOptionalState(pick, label) {
73
+ const module = getModule();
74
+ if (module === null) {
75
+ dlog(`AccessibilityInfo(ios).${label} -> no module (false)`);
76
+ return Promise.resolve(false);
77
+ }
78
+ const getter = pick(module);
79
+ if (getter === undefined) {
80
+ dlog(`AccessibilityInfo(ios).${label} -> method absent (false)`);
81
+ return Promise.resolve(false);
82
+ }
83
+ return new Promise((resolve, reject) => {
84
+ getter.call(module, enabled => resolve(enabled), error => reject(error));
85
+ });
86
+ }
87
+ class AccessibilityInfoIOS {
88
+ isScreenReaderEnabled() {
89
+ return queryState(m => m.getCurrentVoiceOverState, 'isScreenReaderEnabled');
90
+ }
91
+ isReduceMotionEnabled() {
92
+ return queryState(m => m.getCurrentReduceMotionState, 'isReduceMotionEnabled');
93
+ }
94
+ isBoldTextEnabled() {
95
+ return queryState(m => m.getCurrentBoldTextState, 'isBoldTextEnabled');
96
+ }
97
+ isGrayscaleEnabled() {
98
+ return queryState(m => m.getCurrentGrayscaleState, 'isGrayscaleEnabled');
99
+ }
100
+ isInvertColorsEnabled() {
101
+ return queryState(m => m.getCurrentInvertColorsState, 'isInvertColorsEnabled');
102
+ }
103
+ isReduceTransparencyEnabled() {
104
+ return queryState(m => m.getCurrentReduceTransparencyState, 'isReduceTransparencyEnabled');
105
+ }
106
+ // iOS "Increase Contrast": Settings > Accessibility > Display & Text Size. The native
107
+ // getter is optional (older hosts lack it); resolve false when absent rather than reject,
108
+ // keeping the unified surface non-throwing (RN rejects, we mirror the false fallback).
109
+ isDarkerSystemColorsEnabled() {
110
+ return queryOptionalState(m => m.getCurrentDarkerSystemColorsState, 'isDarkerSystemColorsEnabled');
111
+ }
112
+ // iOS reduce-motion sub-setting (prefer cross-fade over slide). Optional native getter;
113
+ // resolve false when absent (RN parity for the unavailable case).
114
+ prefersCrossFadeTransitions() {
115
+ return queryOptionalState(m => m.getCurrentPrefersCrossFadeTransitionsState, 'prefersCrossFadeTransitions');
116
+ }
117
+ // Android-only query; iOS has no high-text-contrast concept, so resolve false (RN parity).
118
+ isHighTextContrastEnabled() {
119
+ return Promise.resolve(false);
120
+ }
121
+ // Android-only query; on iOS RN rejects. We resolve false to keep the unified surface
122
+ // non-throwing; the dlog records that it's a no-op on this platform.
123
+ isAccessibilityServiceEnabled() {
124
+ dlog('AccessibilityInfo(ios).isAccessibilityServiceEnabled -> Android-only (false)');
125
+ return Promise.resolve(false);
126
+ }
127
+ // Post a string to be announced by the screen reader. No-op without a module.
128
+ announceForAccessibility(announcement) {
129
+ const module = getModule();
130
+ if (module === null) {
131
+ dlog('AccessibilityInfo(ios).announceForAccessibility -> no module (no-op)');
132
+ return;
133
+ }
134
+ module.announceForAccessibility(announcement);
135
+ }
136
+ // Announce with queue/priority options. Falls back to the plain announce when the host
137
+ // lacks the options-aware method (older iOS), mirroring RN.
138
+ announceForAccessibilityWithOptions(announcement, options) {
139
+ const module = getModule();
140
+ if (module === null) {
141
+ dlog('AccessibilityInfo(ios).announceForAccessibilityWithOptions -> no module (no-op)');
142
+ return;
143
+ }
144
+ if (module.announceForAccessibilityWithOptions) {
145
+ module.announceForAccessibilityWithOptions(announcement, options);
146
+ }
147
+ else {
148
+ module.announceForAccessibility(announcement);
149
+ }
150
+ }
151
+ // Move accessibility focus to the view with the given react tag. No-op without a module.
152
+ // RN deprecates this in favor of sendAccessibilityEvent; kept for parity.
153
+ setAccessibilityFocus(reactTag) {
154
+ const module = getModule();
155
+ if (module === null) {
156
+ dlog('AccessibilityInfo(ios).setAccessibilityFocus -> no module (no-op)');
157
+ return;
158
+ }
159
+ dlog(`AccessibilityInfo(ios).setAccessibilityFocus -> ${reactTag}`);
160
+ module.setAccessibilityFocus(reactTag);
161
+ }
162
+ // iOS has no recommended-timeout query; resolve the original (RN parity).
163
+ getRecommendedTimeoutMillis(originalTimeout) {
164
+ return Promise.resolve(originalTimeout);
165
+ }
166
+ // Emit an accessibility event at a view through the Fabric slot. RN's Fabric
167
+ // sendAccessibilityEvent hands the public-instance handle straight to
168
+ // nativeFabricUIManager.sendAccessibilityEvent with the STRING eventType, and the C++
169
+ // side maps it. The handle here IS the SymbioteNode (symbiote augments the node in place
170
+ // as its public instance), so resolve it with the runtime guard and route through shared.
171
+ // RN early-returns 'click' on iOS only (AccessibilityInfo.js) because VoiceOver has no click
172
+ // producer, so preserve that one no-op; every other event reaches the slot.
173
+ sendAccessibilityEvent(handle, eventType) {
174
+ if (eventType === 'click') {
175
+ dlog('AccessibilityInfo(ios).sendAccessibilityEvent("click") -> iOS no-op (RN parity)');
176
+ return;
177
+ }
178
+ if (!isSymbioteNode(handle)) {
179
+ dlog(`AccessibilityInfo(ios).sendAccessibilityEvent("${eventType}") -> handle is not a node (no-op)`);
180
+ return;
181
+ }
182
+ dlog(`AccessibilityInfo(ios).sendAccessibilityEvent("${eventType}") -> slot`);
183
+ sharedSendAccessibilityEvent(handle, eventType);
184
+ }
185
+ // Subscribe to an accessibility-state change. A handler for a boolean event receives a
186
+ // boolean; the iOS-only `announcementFinished` carries the announcement payload. Never
187
+ // throws: a public event with no iOS device mapping yields an inert subscription, and a
188
+ // missing module yields a live-but-silent one (the counters are no-ops without a module).
189
+ addEventListener(eventName, handler) {
190
+ const deviceEvent = IOS_DEVICE_EVENT[eventName];
191
+ dlog(`AccessibilityInfo(ios).addEventListener -> ${eventName} (device: ${deviceEvent ?? 'none'})`);
192
+ if (deviceEvent === undefined) {
193
+ return { remove() { } };
194
+ }
195
+ const eventEmitter = getEmitter();
196
+ return eventEmitter.addListener(deviceEvent, payload => {
197
+ // Most events carry a bare boolean; announcementFinished carries an object. Forward
198
+ // each in its own shape, dropping payloads that match neither so we never forward
199
+ // garbage to the handler.
200
+ if (eventName === 'announcementFinished') {
201
+ if (isAnnouncementFinished(payload))
202
+ handler(payload);
203
+ return;
204
+ }
205
+ if (!isBoolean(payload))
206
+ return;
207
+ handler(payload);
208
+ });
209
+ }
210
+ }
211
+ function isAnnouncementFinished(payload) {
212
+ return (typeof payload === 'object' &&
213
+ payload !== null &&
214
+ 'announcement' in payload &&
215
+ typeof payload.announcement === 'string' &&
216
+ 'success' in payload &&
217
+ typeof payload.success === 'boolean');
218
+ }
219
+ export const AccessibilityInfo = new AccessibilityInfoIOS();
@@ -0,0 +1,5 @@
1
+ // Base / default AccessibilityInfo: re-exports the iOS build. Metro overrides this with
2
+ // accessibility-info.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
4
+ // accessibility-info-shared.ts.
5
+ export * from './index.ios';
@@ -0,0 +1,34 @@
1
+ import type { ISymbioteNode } from '../node';
2
+ import type { IEventSubscription } from '../native-events';
3
+ export type IAccessibilityChangeEventName = 'screenReaderChanged' | 'reduceMotionChanged' | 'boldTextChanged' | 'grayscaleChanged' | 'invertColorsChanged' | 'reduceTransparencyChanged' | 'darkerSystemColorsChanged' | 'announcementFinished' | 'accessibilityServiceChanged' | 'highTextContrastChanged';
4
+ export type IAccessibilityChangeEvent = IAccessibilityChangeEventName;
5
+ export type IAccessibilityHandle = ISymbioteNode | number | null | undefined;
6
+ export interface IAccessibilityAnnouncementFinishedEvent {
7
+ announcement: string;
8
+ success: boolean;
9
+ }
10
+ export type IAccessibilityChangeEventHandler = (state: boolean | IAccessibilityAnnouncementFinishedEvent) => void;
11
+ export interface IAnnounceForAccessibilityOptions {
12
+ queue?: boolean;
13
+ priority?: 'low' | 'default' | 'high';
14
+ }
15
+ export interface IAccessibilityInfoStatic {
16
+ isScreenReaderEnabled(): Promise<boolean>;
17
+ isReduceMotionEnabled(): Promise<boolean>;
18
+ isBoldTextEnabled(): Promise<boolean>;
19
+ isGrayscaleEnabled(): Promise<boolean>;
20
+ isInvertColorsEnabled(): Promise<boolean>;
21
+ isReduceTransparencyEnabled(): Promise<boolean>;
22
+ isHighTextContrastEnabled(): Promise<boolean>;
23
+ isAccessibilityServiceEnabled(): Promise<boolean>;
24
+ isDarkerSystemColorsEnabled(): Promise<boolean>;
25
+ prefersCrossFadeTransitions(): Promise<boolean>;
26
+ announceForAccessibility(announcement: string): void;
27
+ announceForAccessibilityWithOptions(announcement: string, options: IAnnounceForAccessibilityOptions): void;
28
+ setAccessibilityFocus(reactTag: number): void;
29
+ getRecommendedTimeoutMillis(originalTimeout: number): Promise<number>;
30
+ sendAccessibilityEvent(handle: IAccessibilityHandle, eventType: IAccessibilityEventType): void;
31
+ addEventListener(eventName: IAccessibilityChangeEventName, handler: IAccessibilityChangeEventHandler): IEventSubscription;
32
+ }
33
+ export type IAccessibilityEventType = 'click' | 'focus' | 'viewHoverEnter' | 'windowStateChange';
34
+ export declare function isBoolean(value: unknown): value is boolean;
@@ -0,0 +1,13 @@
1
+ // AccessibilityInfo, shared contract. The component renders NO Fabric view; it
2
+ // imperatively queries an accessibility native module and subscribes to its device
3
+ // events. What DIVERGES by platform is the native module (iOS `AccessibilityManager`
4
+ // with callback getters; Android stock `AccessibilityInfo` with single-callback
5
+ // getters), which getters exist, and the DEVICE-EVENT NAME a public
6
+ // event maps to (iOS `screenReaderChanged` vs Android `touchExplorationDidChange`).
7
+ // So the .ios/.android files own the native calls and per-platform event-name map;
8
+ // the public types + the shared method surface live here. Filename selects, no
9
+ // Platform.OS read (see ADR 0012 + native_module_name_is_platform_specific). Mirrors
10
+ // RN's Libraries/Components/AccessibilityInfo/AccessibilityInfo.js.
11
+ export function isBoolean(value) {
12
+ return typeof value === 'boolean';
13
+ }
@@ -0,0 +1,36 @@
1
+ export interface IActionSheetIOSOptions {
2
+ title?: string;
3
+ message?: string;
4
+ options: string[];
5
+ destructiveButtonIndex?: number | number[];
6
+ destructiveButtonIndices?: number[];
7
+ cancelButtonIndex?: number;
8
+ anchor?: number;
9
+ tintColor?: unknown;
10
+ cancelButtonTintColor?: unknown;
11
+ disabledButtonTintColor?: unknown;
12
+ userInterfaceStyle?: string;
13
+ disabledButtonIndices?: number[];
14
+ }
15
+ export interface IShareActionSheetIOSOptions {
16
+ message?: string;
17
+ url?: string;
18
+ subject?: string;
19
+ anchor?: number;
20
+ tintColor?: unknown;
21
+ cancelButtonTintColor?: unknown;
22
+ disabledButtonTintColor?: unknown;
23
+ excludedActivityTypes?: string[];
24
+ userInterfaceStyle?: string;
25
+ }
26
+ export interface IShareActionSheetError {
27
+ domain: string;
28
+ code: string;
29
+ userInfo?: Record<string, unknown>;
30
+ message: string;
31
+ }
32
+ export declare const ActionSheetIOS: {
33
+ showActionSheetWithOptions(options: IActionSheetIOSOptions, callback: (buttonIndex: number) => void): void;
34
+ showShareActionSheetWithOptions(options: IShareActionSheetIOSOptions, failureCallback: (error: IShareActionSheetError) => void, successCallback: (completed: boolean, activityType?: string) => void): void;
35
+ dismissActionSheet(): void;
36
+ };