@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
@@ -0,0 +1,222 @@
1
+ // InteractionManager: schedule long-running work to run only after any active
2
+ // interactions/animations have completed, so JS animations stay smooth. A faithful
3
+ // port of React Native's Libraries/Interaction/InteractionManager (the queue-backed
4
+ // implementation, not the deprecated no-op stub).
5
+ //
6
+ // It is pure JS (timers + a tiny event emitter, no React and no native bridge), so
7
+ // per symbiote's layering invariant it lives in @symbiote-native/engine, where every adapter
8
+ // re-exports it.
9
+ //
10
+ // Mechanics: each `runAfterInteractions` task is pushed onto a queue and the queue is
11
+ // flushed on the next tick. `createInteractionHandle` increments an outstanding-handle
12
+ // count that blocks the flush; `clearInteractionHandle` decrements it and, when the
13
+ // last handle clears, schedules a flush on the next tick. `interactionStart` /
14
+ // `interactionComplete` fire as the count crosses 0.
15
+ import { dlog } from '../debug';
16
+ // A positive deadline (ms) makes the flush yield via setTimeout once that much event-
17
+ // loop time has elapsed within one batch, letting touches interrupt; 0 (default) runs
18
+ // the whole batch in one setImmediate.
19
+ const DEFAULT_DEADLINE = 0;
20
+ // setTimeout delay used both to yield to the event loop and to resume after the last
21
+ // handle clears: a 0ms macrotask, never a real wait.
22
+ const NEXT_TICK_MS = 0;
23
+ export const Events = {
24
+ interactionStart: 'interactionStart',
25
+ interactionComplete: 'interactionComplete',
26
+ };
27
+ function isSimpleTask(task) {
28
+ return typeof Reflect.get(task, 'run') === 'function';
29
+ }
30
+ function isPromiseTask(task) {
31
+ return typeof Reflect.get(task, 'gen') === 'function';
32
+ }
33
+ // Minimal string→listener-set emitter; we don't pull in events.ts to keep this module
34
+ // self-contained, and the surface here is just on/off/emit over two event names.
35
+ class Emitter {
36
+ listeners = new Map();
37
+ on(eventType, listener) {
38
+ let set = this.listeners.get(eventType);
39
+ if (set === undefined) {
40
+ set = new Set();
41
+ this.listeners.set(eventType, set);
42
+ }
43
+ set.add(listener);
44
+ return () => {
45
+ set?.delete(listener);
46
+ };
47
+ }
48
+ emit(eventType, ...args) {
49
+ const set = this.listeners.get(eventType);
50
+ if (set === undefined)
51
+ return;
52
+ for (const listener of [...set])
53
+ listener(...args);
54
+ }
55
+ }
56
+ // Lazily resolve Node's setImmediate, falling back to a 0ms setTimeout where it is
57
+ // absent (browsers, some RN hosts). A runtime guard keeps us off `as`.
58
+ function scheduleImmediate(callback) {
59
+ const candidate = Reflect.get(globalThis, 'setImmediate');
60
+ if (typeof candidate === 'function') {
61
+ candidate(callback);
62
+ return;
63
+ }
64
+ scheduleTimeout(callback, NEXT_TICK_MS);
65
+ }
66
+ // The host timer, read lazily off globalThis (shared's tsconfig does not type timers
67
+ // on globalThis, the same reason raf.ts reaches them through Reflect.get).
68
+ function scheduleTimeout(callback, ms) {
69
+ const candidate = Reflect.get(globalThis, 'setTimeout');
70
+ if (typeof candidate === 'function')
71
+ candidate(callback, ms);
72
+ }
73
+ class InteractionManagerImpl {
74
+ Events = Events;
75
+ emitter = new Emitter();
76
+ taskQueue = [];
77
+ interactionHandleCount = 0;
78
+ nextHandle = 1;
79
+ deadline = DEFAULT_DEADLINE;
80
+ flushScheduled = false;
81
+ addListener(eventType, listener) {
82
+ const off = this.emitter.on(eventType, listener);
83
+ return { remove: off };
84
+ }
85
+ // Schedule a task to run once all interaction handles have cleared and the queue
86
+ // drains. Returns a cancellable promise-like.
87
+ runAfterInteractions(task) {
88
+ let resolveTask;
89
+ let rejectTask;
90
+ const promise = new Promise((resolve, reject) => {
91
+ resolveTask = resolve;
92
+ rejectTask = reject;
93
+ });
94
+ const queued = () => {
95
+ this.runTask(task, resolveTask, rejectTask);
96
+ };
97
+ let cancelled = false;
98
+ const guarded = () => {
99
+ if (cancelled)
100
+ return;
101
+ queued();
102
+ };
103
+ this.taskQueue.push(guarded);
104
+ this.scheduleFlush();
105
+ return {
106
+ then: promise.then.bind(promise),
107
+ done: (onDone) => {
108
+ // RN's `done` is `then` run for effect: fire and forget.
109
+ void promise.then(onDone);
110
+ },
111
+ cancel: () => {
112
+ cancelled = true;
113
+ },
114
+ };
115
+ }
116
+ // Notify the manager an interaction has started; defers queued tasks. Returns a
117
+ // handle to clear when the interaction ends.
118
+ createInteractionHandle() {
119
+ dlog('InteractionManager.createInteractionHandle');
120
+ const wasIdle = this.interactionHandleCount === 0;
121
+ this.interactionHandleCount += 1;
122
+ if (wasIdle)
123
+ this.emitter.emit(Events.interactionStart);
124
+ const handle = this.nextHandle;
125
+ this.nextHandle += 1;
126
+ return handle;
127
+ }
128
+ // Notify the manager an interaction has completed; the last clear resumes the queue.
129
+ clearInteractionHandle(handle) {
130
+ if (!handle)
131
+ throw new Error('InteractionManager: Must provide a handle to clear.');
132
+ dlog(`InteractionManager.clearInteractionHandle(${handle})`);
133
+ this.interactionHandleCount -= 1;
134
+ if (this.interactionHandleCount === 0) {
135
+ this.emitter.emit(Events.interactionComplete);
136
+ // Resume on the next tick so synchronous clear→schedule pairs still yield once.
137
+ scheduleTimeout(() => {
138
+ this.scheduleFlush();
139
+ }, NEXT_TICK_MS);
140
+ }
141
+ }
142
+ setDeadline(deadline) {
143
+ this.deadline = deadline;
144
+ }
145
+ runTask(task, resolveTask, rejectTask) {
146
+ const resolve = resolveTask ?? (() => { });
147
+ const reject = rejectTask ?? (() => { });
148
+ if (task === undefined || task === null) {
149
+ resolve();
150
+ return;
151
+ }
152
+ if (typeof task === 'function') {
153
+ try {
154
+ task();
155
+ resolve();
156
+ }
157
+ catch (error) {
158
+ reject(toError(error));
159
+ }
160
+ return;
161
+ }
162
+ if (isPromiseTask(task)) {
163
+ task.gen().then(() => resolve(), reject);
164
+ return;
165
+ }
166
+ if (isSimpleTask(task)) {
167
+ try {
168
+ task.run();
169
+ resolve();
170
+ }
171
+ catch (error) {
172
+ reject(toError(error));
173
+ }
174
+ return;
175
+ }
176
+ reject(new TypeError('InteractionManager task object must have a gen or run method.'));
177
+ }
178
+ // Schedule a single flush of the queue on the next tick, coalescing multiple
179
+ // schedule calls within the same tick into one.
180
+ scheduleFlush() {
181
+ if (this.flushScheduled)
182
+ return;
183
+ if (this.taskQueue.length === 0)
184
+ return;
185
+ this.flushScheduled = true;
186
+ scheduleImmediate(() => {
187
+ this.flushScheduled = false;
188
+ this.flushQueue();
189
+ });
190
+ }
191
+ // Drain the queue while no handles are outstanding, honouring the deadline by
192
+ // yielding via setTimeout when one batch overruns it.
193
+ flushQueue() {
194
+ if (this.interactionHandleCount > 0)
195
+ return;
196
+ const startTime = Date.now();
197
+ while (this.taskQueue.length > 0) {
198
+ if (this.interactionHandleCount > 0)
199
+ return;
200
+ const next = this.taskQueue.shift();
201
+ if (next === undefined)
202
+ break;
203
+ if (typeof next === 'function')
204
+ next();
205
+ if (this.deadline > DEFAULT_DEADLINE && Date.now() - startTime >= this.deadline) {
206
+ if (this.taskQueue.length > 0) {
207
+ scheduleTimeout(() => {
208
+ this.flushQueue();
209
+ }, NEXT_TICK_MS);
210
+ }
211
+ return;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ // Coerce a thrown unknown into an Error without `as`, mirroring RN's toError.
217
+ function toError(value) {
218
+ if (value instanceof Error)
219
+ return value;
220
+ return new Error(typeof value === 'string' ? value : 'Unknown InteractionManager task error');
221
+ }
222
+ export const InteractionManager = new InteractionManagerImpl();
@@ -0,0 +1,31 @@
1
+ import { type IEventSubscription, type INativeEventListener } from '../native-events';
2
+ export declare const KEYBOARD_EVENT: {
3
+ readonly willShow: "keyboardWillShow";
4
+ readonly didShow: "keyboardDidShow";
5
+ readonly willHide: "keyboardWillHide";
6
+ readonly didHide: "keyboardDidHide";
7
+ readonly willChangeFrame: "keyboardWillChangeFrame";
8
+ readonly didChangeFrame: "keyboardDidChangeFrame";
9
+ };
10
+ export type IKeyboardEventName = (typeof KEYBOARD_EVENT)[keyof typeof KEYBOARD_EVENT];
11
+ export interface IKeyboardMetrics {
12
+ screenX: number;
13
+ screenY: number;
14
+ width: number;
15
+ height: number;
16
+ }
17
+ export interface IKeyboardEvent {
18
+ duration: number;
19
+ easing: string;
20
+ endCoordinates: IKeyboardMetrics;
21
+ startCoordinates?: IKeyboardMetrics;
22
+ isEventFromThisApp?: boolean;
23
+ }
24
+ export declare const Keyboard: {
25
+ addListener(eventType: IKeyboardEventName, listener: INativeEventListener): IEventSubscription;
26
+ removeAllListeners(eventType: IKeyboardEventName): void;
27
+ isVisible(): boolean;
28
+ metrics(): IKeyboardMetrics | undefined;
29
+ scheduleLayoutAnimation(event: IKeyboardEvent): void;
30
+ dismiss(): void;
31
+ };
@@ -0,0 +1,142 @@
1
+ // Keyboard module: the first consumer of the native->JS event bridge. Native
2
+ // emits keyboard notifications (show/hide/changeFrame) into the device hub; this
3
+ // subscribes through a NativeEventEmitter bound to the KeyboardObserver native
4
+ // module, which RN keys its keyboard events off of. Mirrors RN's
5
+ // Libraries/Components/Keyboard/Keyboard.js, slimmed to the parts we need.
6
+ import { getNativeModule } from '../native-modules';
7
+ import { installDeviceEventHub, NativeEventEmitter, } from '../native-events';
8
+ import { dlog } from '../debug';
9
+ import { blurTextInput, currentlyFocusedInput } from '../text-input-state';
10
+ import { LayoutAnimation } from '../layout-animation';
11
+ // RN's scheduleLayoutAnimation falls back to the 'keyboard' animation type when the
12
+ // event's easing string isn't a known LayoutAnimation type (Keyboard.js:200).
13
+ const KEYBOARD_ANIMATION_TYPE = 'keyboard';
14
+ // Map a raw easing string onto a ILayoutAnimationType without a cast: RN does
15
+ // `LayoutAnimation.Types[easing] || 'keyboard'`, which only yields a real type when
16
+ // the string is a key of the Types table. We mirror that by checking membership in
17
+ // the frozen table before trusting the value.
18
+ function easingToAnimationType(easing) {
19
+ const types = LayoutAnimation.Types;
20
+ return types[easing] ?? KEYBOARD_ANIMATION_TYPE;
21
+ }
22
+ // The native module name RN registers the keyboard observer under, confirmed
23
+ // from its spec (specs_DEPRECATED/modules/INativeKeyboardObserver.js:20,
24
+ // `TurboModuleRegistry.get('KeyboardObserver')`).
25
+ const KEYBOARD_OBSERVER_MODULE = 'KeyboardObserver';
26
+ // The keyboard notification names native emits. RN's KeyboardEventDefinitions.
27
+ export const KEYBOARD_EVENT = {
28
+ willShow: 'keyboardWillShow',
29
+ didShow: 'keyboardDidShow',
30
+ willHide: 'keyboardWillHide',
31
+ didHide: 'keyboardDidHide',
32
+ willChangeFrame: 'keyboardWillChangeFrame',
33
+ didChangeFrame: 'keyboardDidChangeFrame',
34
+ };
35
+ function isKeyboardEvent(payload) {
36
+ return (typeof payload === 'object' &&
37
+ payload !== null &&
38
+ 'endCoordinates' in payload &&
39
+ typeof payload.endCoordinates === 'object' &&
40
+ payload.endCoordinates !== null);
41
+ }
42
+ // Lazily resolved so importing this module has no native side effect: a headless
43
+ // run without a fake __turboModuleProxy still loads it; resolution happens on the
44
+ // first addListener. Null when the module isn't linked.
45
+ let observer;
46
+ let emitter;
47
+ // The latest keyboardDidShow event, or null when the keyboard is hidden. RN's
48
+ // `_currentlyShowing`. Kept fresh by an internal self-subscription (see getEmitter):
49
+ // Keyboard listens to its OWN show/hide events and caches the event so the synchronous
50
+ // isVisible() / metrics() reads need no native round-trip.
51
+ let currentlyShowing = null;
52
+ // Every live subscription this module handed out, grouped by event type, so
53
+ // removeAllListeners(eventType) can tear them all down. The shared NativeEventEmitter
54
+ // exposes only per-listener remove(), so Keyboard tracks the set itself (mirrors RN's
55
+ // _emitter.removeAllListeners, which we cannot reach through the shared emitter).
56
+ const subscriptions = new Map();
57
+ function trackSubscription(eventType, subscription) {
58
+ let set = subscriptions.get(eventType);
59
+ if (set === undefined) {
60
+ set = new Set();
61
+ subscriptions.set(eventType, set);
62
+ }
63
+ set.add(subscription);
64
+ return {
65
+ remove() {
66
+ subscription.remove();
67
+ set.delete(subscription);
68
+ },
69
+ };
70
+ }
71
+ function getEmitter() {
72
+ if (emitter === undefined) {
73
+ if (observer === undefined) {
74
+ observer = getNativeModule(KEYBOARD_OBSERVER_MODULE);
75
+ dlog(`Keyboard: KeyboardObserver module ${observer ? 'resolved' : 'NOT resolved (null)'}`);
76
+ }
77
+ // WHY lazy: install on the first subscribe rather than at module load, so the
78
+ // hub exists before native emits without a hard bootstrap-order dependency for
79
+ // this first cut. Idempotent, so repeated subscribes cost one boolean check.
80
+ installDeviceEventHub();
81
+ emitter = new NativeEventEmitter(observer ?? undefined);
82
+ // Self-subscription: cache the latest show event, clear on hide (RN's constructor).
83
+ // Bypasses trackSubscription so removeAllListeners never tears down the cache feed.
84
+ emitter.addListener(KEYBOARD_EVENT.didShow, payload => {
85
+ if (isKeyboardEvent(payload))
86
+ currentlyShowing = payload;
87
+ });
88
+ emitter.addListener(KEYBOARD_EVENT.didHide, () => {
89
+ currentlyShowing = null;
90
+ });
91
+ }
92
+ return emitter;
93
+ }
94
+ export const Keyboard = {
95
+ addListener(eventType, listener) {
96
+ dlog(`Keyboard.addListener -> ${eventType}`);
97
+ return trackSubscription(eventType, getEmitter().addListener(eventType, listener));
98
+ },
99
+ // Tear down every listener this module added for one event type. The self-subscription
100
+ // that feeds the cache is untracked, so it survives (RN parity: removeAllListeners only
101
+ // clears caller subscriptions). No-op when nobody's listening for that event.
102
+ removeAllListeners(eventType) {
103
+ dlog(`Keyboard.removeAllListeners -> ${eventType}`);
104
+ const set = subscriptions.get(eventType);
105
+ if (set === undefined)
106
+ return;
107
+ for (const subscription of set)
108
+ subscription.remove();
109
+ set.clear();
110
+ },
111
+ // Whether the keyboard is last known to be visible. Reads the cached show event.
112
+ isVisible() {
113
+ return currentlyShowing !== null;
114
+ },
115
+ // The soft-keyboard frame if visible (the cached event's endCoordinates), else
116
+ // undefined. RN's metrics().
117
+ metrics() {
118
+ return currentlyShowing?.endCoordinates;
119
+ },
120
+ // Syncs an accessory view's layout with the keyboard transition: configure the next
121
+ // commit to animate over the keyboard's own duration/easing. RN's
122
+ // scheduleLayoutAnimation (Keyboard.js:193). Skipped when duration is absent or 0,
123
+ // since a zero-length animation is a no-op.
124
+ scheduleLayoutAnimation(event) {
125
+ const { duration, easing } = event;
126
+ if (duration === 0)
127
+ return;
128
+ dlog(`Keyboard.scheduleLayoutAnimation -> duration ${duration}, easing ${easing}`);
129
+ LayoutAnimation.configureNext({
130
+ duration,
131
+ update: { duration, type: easingToAnimationType(easing) },
132
+ });
133
+ },
134
+ // RN's dismissKeyboard blurs the currently-focused input (TextInputState); blurring
135
+ // an input is what actually retracts the keyboard, so we do the same. A no-op when
136
+ // nothing holds focus, like RN.
137
+ dismiss() {
138
+ const focused = currentlyFocusedInput();
139
+ dlog(`Keyboard.dismiss -> ${focused ? 'blur focused input' : 'no focused input (no-op)'}`);
140
+ blurTextInput(focused);
141
+ },
142
+ };
@@ -0,0 +1,66 @@
1
+ declare const ANIMATION_TYPE: {
2
+ readonly spring: "spring";
3
+ readonly linear: "linear";
4
+ readonly easeInEaseOut: "easeInEaseOut";
5
+ readonly easeIn: "easeIn";
6
+ readonly easeOut: "easeOut";
7
+ readonly keyboard: "keyboard";
8
+ };
9
+ declare const ANIMATION_PROPERTY: {
10
+ readonly opacity: "opacity";
11
+ readonly scaleX: "scaleX";
12
+ readonly scaleY: "scaleY";
13
+ readonly scaleXY: "scaleXY";
14
+ };
15
+ export type ILayoutAnimationType = (typeof ANIMATION_TYPE)[keyof typeof ANIMATION_TYPE];
16
+ export type ILayoutAnimationProperty = (typeof ANIMATION_PROPERTY)[keyof typeof ANIMATION_PROPERTY];
17
+ export type ILayoutAnimationTypes = Readonly<Record<ILayoutAnimationType, ILayoutAnimationType>>;
18
+ export type ILayoutAnimationProperties = Readonly<Record<ILayoutAnimationProperty, ILayoutAnimationProperty>>;
19
+ export interface ILayoutAnimationAnim {
20
+ duration?: number;
21
+ delay?: number;
22
+ springDamping?: number;
23
+ initialVelocity?: number;
24
+ type?: ILayoutAnimationType;
25
+ property?: ILayoutAnimationProperty;
26
+ }
27
+ export interface ILayoutAnimationConfig {
28
+ duration: number;
29
+ create?: ILayoutAnimationAnim;
30
+ update?: ILayoutAnimationAnim;
31
+ delete?: ILayoutAnimationAnim;
32
+ }
33
+ type IOnAnimationDidEndCallback = () => void;
34
+ type IOnAnimationDidFailCallback = () => void;
35
+ declare class LayoutAnimationImpl {
36
+ readonly Types: ILayoutAnimationTypes;
37
+ readonly Properties: ILayoutAnimationProperties;
38
+ readonly Presets: {
39
+ readonly easeInEaseOut: ILayoutAnimationConfig;
40
+ readonly linear: ILayoutAnimationConfig;
41
+ readonly spring: {
42
+ readonly duration: 700;
43
+ readonly create: {
44
+ readonly type: "linear";
45
+ readonly property: "opacity";
46
+ };
47
+ readonly update: {
48
+ readonly type: "spring";
49
+ readonly springDamping: 0.4;
50
+ };
51
+ readonly delete: {
52
+ readonly type: "linear";
53
+ readonly property: "opacity";
54
+ };
55
+ };
56
+ };
57
+ configureNext(config: ILayoutAnimationConfig, onAnimationDidEnd?: IOnAnimationDidEndCallback, onAnimationDidFail?: IOnAnimationDidFailCallback): void;
58
+ create(duration: number, type?: ILayoutAnimationType, property?: ILayoutAnimationProperty): ILayoutAnimationConfig;
59
+ easeInEaseOut(onAnimationDidEnd?: IOnAnimationDidEndCallback): void;
60
+ linear(onAnimationDidEnd?: IOnAnimationDidEndCallback): void;
61
+ spring(onAnimationDidEnd?: IOnAnimationDidEndCallback): void;
62
+ setLayoutAnimationEnabled(enabled: boolean): void;
63
+ checkConfig(..._args: unknown[]): void;
64
+ }
65
+ export declare const LayoutAnimation: LayoutAnimationImpl;
66
+ export {};
@@ -0,0 +1,183 @@
1
+ // LayoutAnimation: configures the NEXT commit to animate layout changes. The
2
+ // animation itself is performed natively; this module only ships a config to the
3
+ // native UIManager via `configureNextLayoutAnimation(config, onSuccess, onError)`
4
+ // before the commit. Ports RN's Libraries/LayoutAnimation/LayoutAnimation.js
5
+ // (the JS surface + the native configure call), built on symbiote's single
6
+ // native trust boundary, `getNativeModule`.
7
+ //
8
+ // It is a native-bridge consumer (RN's `Platform`/`StyleSheet` purity split puts
9
+ // these in the adapter, not in shared, like Keyboard / StatusBar).
10
+ import { getNativeModule } from '../native-modules';
11
+ import { dlog } from '../debug';
12
+ // ---- native module routing ----------------------------------------------
13
+ // DEVICE-VERIFY-PENDING: on bridgeless Fabric the layout-animation configure call
14
+ // is exposed by RN through the UIManager surface (RN's non-Fabric path calls
15
+ // `UIManager.configureNextLayoutAnimation`; the Fabric path routes the same args
16
+ // onto `global.nativeFabricUIManager.configureNextLayoutAnimation`). Per
17
+ // .docs/decisions/0012 and .docs/native-module-platform-routing.md the native
18
+ // MODULE NAME is platform-specific and a headless fake answers to ANY name, so
19
+ // the name below is the most plausible bridgeless candidate, NOT proven. Only the
20
+ // simulator/device resolution log can confirm it; the fallback list is tried in
21
+ // order so a wrong primary still resolves on a real host. Verify on-device before
22
+ // trusting either name. See the `dlog` at the resolution seam below.
23
+ const NATIVE_UI_MANAGER_NAME = {
24
+ primary: 'UIManager',
25
+ fallback: 'FabricUIManager',
26
+ };
27
+ // ---- public type surface (ported from RN's ReactNativeTypes) -------------
28
+ const ANIMATION_TYPE = {
29
+ spring: 'spring',
30
+ linear: 'linear',
31
+ easeInEaseOut: 'easeInEaseOut',
32
+ easeIn: 'easeIn',
33
+ easeOut: 'easeOut',
34
+ keyboard: 'keyboard',
35
+ };
36
+ const ANIMATION_PROPERTY = {
37
+ opacity: 'opacity',
38
+ scaleX: 'scaleX',
39
+ scaleY: 'scaleY',
40
+ scaleXY: 'scaleXY',
41
+ };
42
+ // Resolved FRESH on every configureNext, deliberately NOT memoized. The native module can
43
+ // be absent at one moment and linked later, and a cached answer would pin the first result;
44
+ // getNativeModule is a cheap proxy lookup, so per-call resolution costs nothing and stays
45
+ // correct. (Memoizing here also broke the headless smoke, which flips a fake module on/off
46
+ // in one process: a cached module survived the flip-off, so configureNext kept calling native
47
+ // when the module was meant to be absent.)
48
+ function resolveUIManager() {
49
+ // Try the most plausible bridgeless name first, then fall back. A wrong primary
50
+ // resolves null on a real host (the bridgeless proxy returns null for an
51
+ // unlinked name), so the fallback covers a misnamed primary.
52
+ const candidates = [NATIVE_UI_MANAGER_NAME.primary, NATIVE_UI_MANAGER_NAME.fallback];
53
+ for (const name of candidates) {
54
+ const module = getNativeModule(name);
55
+ if (module !== null) {
56
+ // DEVICE-VERIFY-PENDING seam: this line on the simulator/device is the only
57
+ // proof the chosen name is the real one. Keep it permanently (P0 logging gate).
58
+ dlog(`LayoutAnimation: resolved native UIManager via "${name}"`);
59
+ return module;
60
+ }
61
+ }
62
+ dlog(`LayoutAnimation: no native UIManager resolved (tried ${candidates.join(', ')}); ` +
63
+ `configureNext is a no-op (headless or module not linked)`);
64
+ return null;
65
+ }
66
+ // ---- config builder ------------------------------------------------------
67
+ // Builds a well-formed config for `configureNext`. Mirrors RN's `create`:
68
+ // `create`/`delete` carry both type and property; `update` carries only type.
69
+ function createLayoutAnimation(duration, type, property) {
70
+ return {
71
+ duration,
72
+ create: { type, property },
73
+ update: { type },
74
+ delete: { type, property },
75
+ };
76
+ }
77
+ // ---- presets -------------------------------------------------------------
78
+ const PRESET_DURATION = {
79
+ easeInEaseOut: 300,
80
+ linear: 500,
81
+ spring: 700,
82
+ };
83
+ const SPRING_DAMPING = 0.4;
84
+ const Presets = {
85
+ easeInEaseOut: createLayoutAnimation(PRESET_DURATION.easeInEaseOut, ANIMATION_TYPE.easeInEaseOut, ANIMATION_PROPERTY.opacity),
86
+ linear: createLayoutAnimation(PRESET_DURATION.linear, ANIMATION_TYPE.linear, ANIMATION_PROPERTY.opacity),
87
+ spring: {
88
+ duration: PRESET_DURATION.spring,
89
+ create: { type: ANIMATION_TYPE.linear, property: ANIMATION_PROPERTY.opacity },
90
+ update: { type: ANIMATION_TYPE.spring, springDamping: SPRING_DAMPING },
91
+ delete: { type: ANIMATION_TYPE.linear, property: ANIMATION_PROPERTY.opacity },
92
+ },
93
+ };
94
+ // ---- enabled gate --------------------------------------------------------
95
+ // Whether `configureNext` actually arms the next commit. RN seeds this from a
96
+ // feature flag (LayoutAnimation.js:45) and exposes `setEnabled` to toggle it;
97
+ // here it defaults on (our Fabric path always supports it) and `configureNext`
98
+ // short-circuits when it is off, mirroring RN's `if (!isLayoutAnimationEnabled)
99
+ // return` (LayoutAnimation.js:69).
100
+ let isLayoutAnimationEnabled = true;
101
+ // Gates whether the next commit animates. RN's own `setLayoutAnimationEnabled`
102
+ // (LayoutAnimation.js:48) is a no-op due to a self-assignment bug; we implement
103
+ // the intended behaviour: a disabled state makes `configureNext` a no-op.
104
+ function setLayoutAnimationEnabled(value) {
105
+ isLayoutAnimationEnabled = value;
106
+ }
107
+ // ---- configureNext -------------------------------------------------------
108
+ // Configures the next commit to be animated. NATIVE drives completion:
109
+ // `onAnimationDidEnd` is passed straight through as the native success callback,
110
+ // so it fires exactly when the native animation actually finishes, including
111
+ // when native extends it past `duration` (spring overshoot, OS slowdown,
112
+ // reduce-motion). `onAnimationDidFail` fires only if native config parsing fails.
113
+ // When no native module is linked (headless), this is a logged no-op; an app
114
+ // without it must still run, so we never throw.
115
+ //
116
+ // We deliberately do NOT arm a JS `setTimeout(duration + slack)` to force
117
+ // completion. RN keeps such a timer as a fallback for platform/renderer combos
118
+ // where native never calls back (non-Fabric Android, iOS Fabric pre-ship); but a
119
+ // fixed `duration + 17ms` timer races and usually beats the real native callback,
120
+ // firing `onAnimationDidEnd` before the animation visually completes. On our
121
+ // Fabric-only path native completion is reliably wired, so we rely on it solely.
122
+ function configureNext(config, onAnimationDidEnd, onAnimationDidFail) {
123
+ // RN bails before touching native when animations are disabled
124
+ // (LayoutAnimation.js:69).
125
+ if (!isLayoutAnimationEnabled) {
126
+ dlog('LayoutAnimation.configureNext: disabled; no-op');
127
+ return;
128
+ }
129
+ const manager = resolveUIManager();
130
+ if (manager === null || manager.configureNextLayoutAnimation === undefined) {
131
+ dlog('LayoutAnimation.configureNext: no native UIManager; no-op');
132
+ return;
133
+ }
134
+ // Idempotent guard so native can't drive both success and error into a
135
+ // double-fire (RN's `animationCompletionHasRun`), without a JS timer racing it.
136
+ let completionHasRun = false;
137
+ const onComplete = () => {
138
+ if (completionHasRun)
139
+ return;
140
+ completionHasRun = true;
141
+ onAnimationDidEnd?.();
142
+ };
143
+ dlog(`LayoutAnimation.configureNext: dispatching config (duration=${config.duration})`);
144
+ // onError only fires if native config parsing fails; default to a no-op.
145
+ manager.configureNextLayoutAnimation(config, onComplete, onAnimationDidFail ?? (() => { }));
146
+ }
147
+ // ---- the LayoutAnimation facade ------------------------------------------
148
+ class LayoutAnimationImpl {
149
+ // Frozen so callers can't mutate the shared type/property tables.
150
+ Types = Object.freeze({ ...ANIMATION_TYPE });
151
+ Properties = Object.freeze({ ...ANIMATION_PROPERTY });
152
+ Presets = Presets;
153
+ // Methods (not class fields) so they stay overridable under
154
+ // useDefineForClassFields.
155
+ configureNext(config, onAnimationDidEnd, onAnimationDidFail) {
156
+ configureNext(config, onAnimationDidEnd, onAnimationDidFail);
157
+ }
158
+ create(duration, type, property) {
159
+ return createLayoutAnimation(duration, type, property);
160
+ }
161
+ easeInEaseOut(onAnimationDidEnd) {
162
+ configureNext(Presets.easeInEaseOut, onAnimationDidEnd);
163
+ }
164
+ linear(onAnimationDidEnd) {
165
+ configureNext(Presets.linear, onAnimationDidEnd);
166
+ }
167
+ spring(onAnimationDidEnd) {
168
+ configureNext(Presets.spring, onAnimationDidEnd);
169
+ }
170
+ // RN exposes this as both `setLayoutAnimationEnabled` and the `setEnabled`
171
+ // alias (LayoutAnimation.js:48,222); we surface the primary name. A disabled
172
+ // state makes `configureNext` a no-op.
173
+ setLayoutAnimationEnabled(enabled) {
174
+ setLayoutAnimationEnabled(enabled);
175
+ }
176
+ // RN's dev-time config validator. It has been retired upstream
177
+ // (LayoutAnimation.js:204); the live impl only logs that it is disabled, so
178
+ // we mirror that and keep the call a no-op rather than re-add dead validation.
179
+ checkConfig(..._args) {
180
+ dlog('LayoutAnimation.checkConfig(...) has been disabled.');
181
+ }
182
+ }
183
+ export const LayoutAnimation = new LayoutAnimationImpl();
@@ -0,0 +1,2 @@
1
+ export type { IUrlEvent, IIntentExtra } from './shared';
2
+ export declare const Linking: import("./shared").ILinkingStatic;
@@ -0,0 +1,18 @@
1
+ // Linking: Android build. The native module is `IntentAndroid`
2
+ // (RN's TurboModuleRegistry.get('IntentAndroid'), spec NativeIntentAndroid): the same
3
+ // four URL methods as iOS plus `sendIntent(action, extras?)`. Everything else is the
4
+ // shared core. Metro picks this file on an Android host.
5
+ //
6
+ // device-verify-pending: the `IntentAndroid` name and routing are confirmed from RN
7
+ // source but not yet exercised on a real Android host; only a bridgeless resolution
8
+ // log there can prove the name. See .docs/native-module-platform-routing.md.
9
+ import { createLinking } from './shared';
10
+ export const Linking = createLinking({
11
+ moduleName: 'IntentAndroid',
12
+ sendIntent: (module, action, extras) => {
13
+ if (module === null || module.sendIntent === undefined) {
14
+ return Promise.reject(new Error('Linking: IntentAndroid native module unavailable'));
15
+ }
16
+ return module.sendIntent(action, extras);
17
+ },
18
+ });
@@ -0,0 +1 @@
1
+ export * from './index.ios';
@@ -0,0 +1,2 @@
1
+ export type { IUrlEvent, IIntentExtra } from './shared';
2
+ export declare const Linking: import("./shared").ILinkingStatic;