@symbiote-native/angular 0.0.0-canary-20260724081123

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 (276) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/babel-linker.cjs +7 -0
  4. package/babel-register-composed.cjs +127 -0
  5. package/bin/symbiote-angular-dev.cjs +93 -0
  6. package/build/angular/anchor-host-registry.d.ts +2 -0
  7. package/build/angular/anchor-host-registry.js +84 -0
  8. package/build/angular/anchor-host-registry.js.map +1 -0
  9. package/build/angular/bootstrap.d.ts +7 -0
  10. package/build/angular/bootstrap.js +15 -0
  11. package/build/angular/bootstrap.js.map +1 -0
  12. package/build/angular/components/activity-indicator/index.android.d.ts +12 -0
  13. package/build/angular/components/activity-indicator/index.android.js +21 -0
  14. package/build/angular/components/activity-indicator/index.android.js.map +1 -0
  15. package/build/angular/components/activity-indicator/index.d.ts +1 -0
  16. package/build/angular/components/activity-indicator/index.ios.d.ts +8 -0
  17. package/build/angular/components/activity-indicator/index.ios.js +21 -0
  18. package/build/angular/components/activity-indicator/index.ios.js.map +1 -0
  19. package/build/angular/components/activity-indicator/index.js +4 -0
  20. package/build/angular/components/activity-indicator/index.js.map +1 -0
  21. package/build/angular/components/activity-indicator/shared.d.ts +67 -0
  22. package/build/angular/components/activity-indicator/shared.js +260 -0
  23. package/build/angular/components/activity-indicator/shared.js.map +1 -0
  24. package/build/angular/components/button.d.ts +63 -0
  25. package/build/angular/components/button.js +332 -0
  26. package/build/angular/components/button.js.map +1 -0
  27. package/build/angular/components/flat-list/index.d.ts +122 -0
  28. package/build/angular/components/flat-list/index.js +688 -0
  29. package/build/angular/components/flat-list/index.js.map +1 -0
  30. package/build/angular/components/image/index.android.d.ts +10 -0
  31. package/build/angular/components/image/index.android.js +59 -0
  32. package/build/angular/components/image/index.android.js.map +1 -0
  33. package/build/angular/components/image/index.d.ts +1 -0
  34. package/build/angular/components/image/index.ios.d.ts +10 -0
  35. package/build/angular/components/image/index.ios.js +59 -0
  36. package/build/angular/components/image/index.ios.js.map +1 -0
  37. package/build/angular/components/image/index.js +3 -0
  38. package/build/angular/components/image/index.js.map +1 -0
  39. package/build/angular/components/image/shared.d.ts +101 -0
  40. package/build/angular/components/image/shared.js +406 -0
  41. package/build/angular/components/image/shared.js.map +1 -0
  42. package/build/angular/components/image-background.d.ts +81 -0
  43. package/build/angular/components/image-background.js +418 -0
  44. package/build/angular/components/image-background.js.map +1 -0
  45. package/build/angular/components/input-accessory-view/index.d.ts +60 -0
  46. package/build/angular/components/input-accessory-view/index.js +251 -0
  47. package/build/angular/components/input-accessory-view/index.js.map +1 -0
  48. package/build/angular/components/keyboard-avoiding-view/index.d.ts +82 -0
  49. package/build/angular/components/keyboard-avoiding-view/index.js +358 -0
  50. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -0
  51. package/build/angular/components/modal/index.d.ts +92 -0
  52. package/build/angular/components/modal/index.js +360 -0
  53. package/build/angular/components/modal/index.js.map +1 -0
  54. package/build/angular/components/pressable/index.d.ts +123 -0
  55. package/build/angular/components/pressable/index.js +420 -0
  56. package/build/angular/components/pressable/index.js.map +1 -0
  57. package/build/angular/components/refresh-control/index.d.ts +86 -0
  58. package/build/angular/components/refresh-control/index.js +342 -0
  59. package/build/angular/components/refresh-control/index.js.map +1 -0
  60. package/build/angular/components/safe-area-view/index.d.ts +57 -0
  61. package/build/angular/components/safe-area-view/index.js +248 -0
  62. package/build/angular/components/safe-area-view/index.js.map +1 -0
  63. package/build/angular/components/scroll-view/index.android.d.ts +21 -0
  64. package/build/angular/components/scroll-view/index.android.js +235 -0
  65. package/build/angular/components/scroll-view/index.android.js.map +1 -0
  66. package/build/angular/components/scroll-view/index.d.ts +3 -0
  67. package/build/angular/components/scroll-view/index.ios.d.ts +9 -0
  68. package/build/angular/components/scroll-view/index.ios.js +86 -0
  69. package/build/angular/components/scroll-view/index.ios.js.map +1 -0
  70. package/build/angular/components/scroll-view/index.js +6 -0
  71. package/build/angular/components/scroll-view/index.js.map +1 -0
  72. package/build/angular/components/scroll-view/projection.d.ts +32 -0
  73. package/build/angular/components/scroll-view/projection.js +334 -0
  74. package/build/angular/components/scroll-view/projection.js.map +1 -0
  75. package/build/angular/components/scroll-view/shared.d.ts +230 -0
  76. package/build/angular/components/scroll-view/shared.js +682 -0
  77. package/build/angular/components/scroll-view/shared.js.map +1 -0
  78. package/build/angular/components/scroll-view/sticky-header.d.ts +40 -0
  79. package/build/angular/components/scroll-view/sticky-header.js +225 -0
  80. package/build/angular/components/scroll-view/sticky-header.js.map +1 -0
  81. package/build/angular/components/section-list/index.d.ts +116 -0
  82. package/build/angular/components/section-list/index.js +641 -0
  83. package/build/angular/components/section-list/index.js.map +1 -0
  84. package/build/angular/components/switch/index.android.d.ts +20 -0
  85. package/build/angular/components/switch/index.android.js +70 -0
  86. package/build/angular/components/switch/index.android.js.map +1 -0
  87. package/build/angular/components/switch/index.d.ts +1 -0
  88. package/build/angular/components/switch/index.ios.d.ts +19 -0
  89. package/build/angular/components/switch/index.ios.js +69 -0
  90. package/build/angular/components/switch/index.ios.js.map +1 -0
  91. package/build/angular/components/switch/index.js +4 -0
  92. package/build/angular/components/switch/index.js.map +1 -0
  93. package/build/angular/components/switch/shared.d.ts +40 -0
  94. package/build/angular/components/switch/shared.js +127 -0
  95. package/build/angular/components/switch/shared.js.map +1 -0
  96. package/build/angular/components/text-input.d.ts +153 -0
  97. package/build/angular/components/text-input.js +599 -0
  98. package/build/angular/components/text-input.js.map +1 -0
  99. package/build/angular/components/touchable/index.d.ts +245 -0
  100. package/build/angular/components/touchable/index.js +1186 -0
  101. package/build/angular/components/touchable/index.js.map +1 -0
  102. package/build/angular/components/touchable-native-feedback/index.d.ts +83 -0
  103. package/build/angular/components/touchable-native-feedback/index.js +371 -0
  104. package/build/angular/components/touchable-native-feedback/index.js.map +1 -0
  105. package/build/angular/components/virtualized-list/directives.d.ts +54 -0
  106. package/build/angular/components/virtualized-list/directives.js +153 -0
  107. package/build/angular/components/virtualized-list/directives.js.map +1 -0
  108. package/build/angular/components/virtualized-list/index.d.ts +253 -0
  109. package/build/angular/components/virtualized-list/index.js +1031 -0
  110. package/build/angular/components/virtualized-list/index.js.map +1 -0
  111. package/build/angular/components/virtualized-section-list/directives.d.ts +37 -0
  112. package/build/angular/components/virtualized-section-list/directives.js +76 -0
  113. package/build/angular/components/virtualized-section-list/directives.js.map +1 -0
  114. package/build/angular/components/virtualized-section-list/index.d.ts +178 -0
  115. package/build/angular/components/virtualized-section-list/index.js +720 -0
  116. package/build/angular/components/virtualized-section-list/index.js.map +1 -0
  117. package/build/angular/components.d.ts +39 -0
  118. package/build/angular/components.js +38 -0
  119. package/build/angular/components.js.map +1 -0
  120. package/build/angular/create-portal/index.d.ts +22 -0
  121. package/build/angular/create-portal/index.js +74 -0
  122. package/build/angular/create-portal/index.js.map +1 -0
  123. package/build/angular/create-tunnel/index.d.ts +34 -0
  124. package/build/angular/create-tunnel/index.js +126 -0
  125. package/build/angular/create-tunnel/index.js.map +1 -0
  126. package/build/angular/descriptor-to-angular/index.d.ts +22 -0
  127. package/build/angular/descriptor-to-angular/index.js +142 -0
  128. package/build/angular/descriptor-to-angular/index.js.map +1 -0
  129. package/build/angular/host-instance/index.d.ts +2 -0
  130. package/build/angular/host-instance/index.js +37 -0
  131. package/build/angular/host-instance/index.js.map +1 -0
  132. package/build/angular/index.d.ts +22 -0
  133. package/build/angular/index.js +46 -0
  134. package/build/angular/index.js.map +1 -0
  135. package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +14 -0
  136. package/build/angular/modules/animated/animated-leaf-binder/index.js +91 -0
  137. package/build/angular/modules/animated/animated-leaf-binder/index.js.map +1 -0
  138. package/build/angular/modules/animated/animated-leaf-binder.d.ts +14 -0
  139. package/build/angular/modules/animated/animated-leaf-binder.js +91 -0
  140. package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -0
  141. package/build/angular/modules/animated/create-animated-component.d.ts +59 -0
  142. package/build/angular/modules/animated/create-animated-component.js +364 -0
  143. package/build/angular/modules/animated/create-animated-component.js.map +1 -0
  144. package/build/angular/modules/animated/index.d.ts +51 -0
  145. package/build/angular/modules/animated/index.js +59 -0
  146. package/build/angular/modules/animated/index.js.map +1 -0
  147. package/build/angular/modules/app-registry/index.d.ts +7 -0
  148. package/build/angular/modules/app-registry/index.js +23 -0
  149. package/build/angular/modules/app-registry/index.js.map +1 -0
  150. package/build/angular/modules/status-bar/index.d.ts +20 -0
  151. package/build/angular/modules/status-bar/index.js +59 -0
  152. package/build/angular/modules/status-bar/index.js.map +1 -0
  153. package/build/angular/primitives/index.d.ts +63 -0
  154. package/build/angular/primitives/index.js +189 -0
  155. package/build/angular/primitives/index.js.map +1 -0
  156. package/build/angular/primitives/shared.d.ts +88 -0
  157. package/build/angular/primitives/shared.js +171 -0
  158. package/build/angular/primitives/shared.js.map +1 -0
  159. package/build/angular/render/index.d.ts +11 -0
  160. package/build/angular/render/index.js +171 -0
  161. package/build/angular/render/index.js.map +1 -0
  162. package/build/angular/renderer/index.d.ts +37 -0
  163. package/build/angular/renderer/index.js +301 -0
  164. package/build/angular/renderer/index.js.map +1 -0
  165. package/build/angular/services/color-scheme.service.d.ts +16 -0
  166. package/build/angular/services/color-scheme.service.js +31 -0
  167. package/build/angular/services/color-scheme.service.js.map +1 -0
  168. package/build/angular/services/index.d.ts +2 -0
  169. package/build/angular/services/index.js +3 -0
  170. package/build/angular/services/index.js.map +1 -0
  171. package/build/angular/services/window-dimensions.service.d.ts +16 -0
  172. package/build/angular/services/window-dimensions.service.js +42 -0
  173. package/build/angular/services/window-dimensions.service.js.map +1 -0
  174. package/metro-config.cjs +48 -0
  175. package/metro-css-parser.cjs +13 -0
  176. package/package.json +80 -0
  177. package/src/__tests__/responder-change-detection.test.ts +102 -0
  178. package/src/__tests__/responder-nested-cd.test.ts +113 -0
  179. package/src/anchor-host-registry.ts +88 -0
  180. package/src/angular-gaps.test.ts +142 -0
  181. package/src/angular-imports.test.ts +100 -0
  182. package/src/bootstrap.ts +26 -0
  183. package/src/components/activity-indicator/activity-indicator-class-style.test.ts +44 -0
  184. package/src/components/activity-indicator/activity-indicator.test.ts +131 -0
  185. package/src/components/activity-indicator/index.android.ts +16 -0
  186. package/src/components/activity-indicator/index.ios.ts +17 -0
  187. package/src/components/activity-indicator/index.ts +4 -0
  188. package/src/components/activity-indicator/shared.ts +206 -0
  189. package/src/components/button.ts +193 -0
  190. package/src/components/flat-list/column-wrapper-style-class.test.ts +116 -0
  191. package/src/components/flat-list/flat-list-array-style.test.ts +131 -0
  192. package/src/components/flat-list/flat-list-class-style.test.ts +74 -0
  193. package/src/components/flat-list/flat-list-scroll-containment.test.ts +107 -0
  194. package/src/components/flat-list/flat-list.test.ts +99 -0
  195. package/src/components/flat-list/index.ts +574 -0
  196. package/src/components/image/image-class-style.test.ts +46 -0
  197. package/src/components/image/index.android.ts +48 -0
  198. package/src/components/image/index.ios.ts +48 -0
  199. package/src/components/image/index.ts +3 -0
  200. package/src/components/image/shared.ts +479 -0
  201. package/src/components/image-background-class-style.test.ts +48 -0
  202. package/src/components/image-background-style-class.test.ts +79 -0
  203. package/src/components/image-background.ts +270 -0
  204. package/src/components/input-accessory-view/index.ts +185 -0
  205. package/src/components/input-accessory-view/input-accessory-view.test.ts +43 -0
  206. package/src/components/keyboard-avoiding-view/index.ts +303 -0
  207. package/src/components/keyboard-avoiding-view/keyboard-avoiding-view.test.ts +54 -0
  208. package/src/components/modal/index.ts +299 -0
  209. package/src/components/modal/modal.test.ts +43 -0
  210. package/src/components/pressable/index.ts +373 -0
  211. package/src/components/pressable/pressable.test.ts +85 -0
  212. package/src/components/refresh-control/index.ts +284 -0
  213. package/src/components/refresh-control/refresh-control.test.ts +41 -0
  214. package/src/components/safe-area-view/index.ts +177 -0
  215. package/src/components/safe-area-view/safe-area-view.test.ts +43 -0
  216. package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +123 -0
  217. package/src/components/scroll-view/content-container-style-class.test.ts +75 -0
  218. package/src/components/scroll-view/index.android.ts +190 -0
  219. package/src/components/scroll-view/index.ios.ts +79 -0
  220. package/src/components/scroll-view/index.ts +7 -0
  221. package/src/components/scroll-view/projection.ts +414 -0
  222. package/src/components/scroll-view/scroll-view-class-style.test.ts +134 -0
  223. package/src/components/scroll-view/scroll-view-projection.test.ts +194 -0
  224. package/src/components/scroll-view/shared.ts +844 -0
  225. package/src/components/scroll-view/sticky-header.ts +255 -0
  226. package/src/components/section-list/index.ts +445 -0
  227. package/src/components/switch/android-switch-class.test.ts +42 -0
  228. package/src/components/switch/index.android.ts +61 -0
  229. package/src/components/switch/index.ios.ts +60 -0
  230. package/src/components/switch/index.ts +4 -0
  231. package/src/components/switch/shared.ts +197 -0
  232. package/src/components/switch/switch.test.ts +92 -0
  233. package/src/components/text-input-class-style.test.ts +54 -0
  234. package/src/components/text-input.ts +531 -0
  235. package/src/components/touchable/index.ts +629 -0
  236. package/src/components/touchable/touchable.test.ts +102 -0
  237. package/src/components/touchable-native-feedback/index.ts +254 -0
  238. package/src/components/touchable-native-feedback/touchable-native-feedback.test.ts +49 -0
  239. package/src/components/virtualized-list/directives.ts +168 -0
  240. package/src/components/virtualized-list/index.ts +992 -0
  241. package/src/components/virtualized-list/virtualized-list-class-style.test.ts +76 -0
  242. package/src/components/virtualized-section-list/directives.ts +90 -0
  243. package/src/components/virtualized-section-list/index.ts +601 -0
  244. package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +60 -0
  245. package/src/components.ts +149 -0
  246. package/src/create-portal/create-portal.test.ts +90 -0
  247. package/src/create-portal/index.ts +74 -0
  248. package/src/create-tunnel/create-tunnel.test.ts +120 -0
  249. package/src/create-tunnel/index.ts +136 -0
  250. package/src/descriptor-to-angular/descriptor-outlet.test.ts +94 -0
  251. package/src/descriptor-to-angular/index.ts +190 -0
  252. package/src/host-instance/host-instance.test.ts +49 -0
  253. package/src/host-instance/index.ts +36 -0
  254. package/src/index.ts +261 -0
  255. package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +116 -0
  256. package/src/modules/animated/animated-leaf-binder/index.ts +101 -0
  257. package/src/modules/animated/animated-leaf-binder.test.ts +116 -0
  258. package/src/modules/animated/animated-leaf-binder.ts +101 -0
  259. package/src/modules/animated/animated-scroll-view.test.ts +102 -0
  260. package/src/modules/animated/create-animated-component.ts +365 -0
  261. package/src/modules/animated/index.ts +105 -0
  262. package/src/modules/app-registry/app-registry.test.ts +106 -0
  263. package/src/modules/app-registry/index.ts +60 -0
  264. package/src/modules/status-bar/index.ts +57 -0
  265. package/src/modules/status-bar/status-bar.test.ts +50 -0
  266. package/src/primitives/index.ts +119 -0
  267. package/src/primitives/shared.ts +190 -0
  268. package/src/render/index.ts +235 -0
  269. package/src/render/render.test.ts +147 -0
  270. package/src/renderer/index.ts +347 -0
  271. package/src/renderer/renderer.test.ts +203 -0
  272. package/src/services/color-scheme.service.ts +27 -0
  273. package/src/services/index.ts +2 -0
  274. package/src/services/services.test.ts +163 -0
  275. package/src/services/window-dimensions.service.ts +41 -0
  276. package/tsconfig.angular.base.json +30 -0
@@ -0,0 +1,57 @@
1
+ // StatusBar, the Angular lifecycle half. The native StatusBarManager driving, the imperative
2
+ // statics, and the Android bar-height constant all live in @symbiote-native/engine, shared verbatim with
3
+ // React and Vue; Metro selects the engine's platform variant. Angular supplies only the
4
+ // declarative shape: a component that renders nothing and re-applies the props through
5
+ // ngOnChanges on mount + every prop change. Imperative statics are attached to the component
6
+ // object the same way RN does.
7
+
8
+ import { Component, Input, type OnChanges, type SimpleChanges } from '@angular/core';
9
+ import {
10
+ applyStatusBarProps,
11
+ statusBarImperative,
12
+ statusBarCurrentHeight,
13
+ type IColorValue,
14
+ type IStatusBarProps,
15
+ type IStatusBarStyle,
16
+ } from '@symbiote-native/engine';
17
+
18
+ export type { IStatusBarProps, IStatusBarStyle } from '@symbiote-native/engine';
19
+
20
+ @Component({
21
+ selector: 'StatusBar',
22
+ standalone: true,
23
+ template: '',
24
+ })
25
+ class StatusBarComponent implements OnChanges {
26
+ @Input() barStyle?: IStatusBarStyle;
27
+ @Input() hidden?: boolean;
28
+ @Input() animated?: boolean;
29
+ @Input() networkActivityIndicatorVisible?: boolean;
30
+ @Input() backgroundColor?: IColorValue;
31
+ @Input() translucent?: boolean;
32
+
33
+ ngOnChanges(_changes: SimpleChanges): void {
34
+ applyStatusBarProps(this.buildProps());
35
+ }
36
+
37
+ private buildProps(): IStatusBarProps {
38
+ return {
39
+ barStyle: this.barStyle,
40
+ hidden: this.hidden,
41
+ animated: this.animated,
42
+ networkActivityIndicatorVisible: this.networkActivityIndicatorVisible,
43
+ backgroundColor: this.backgroundColor,
44
+ translucent: this.translucent,
45
+ };
46
+ }
47
+ }
48
+
49
+ const StatusBarWithStatics = Object.assign(StatusBarComponent, statusBarImperative);
50
+
51
+ Object.defineProperty(StatusBarWithStatics, 'currentHeight', {
52
+ get: statusBarCurrentHeight,
53
+ enumerable: true,
54
+ });
55
+
56
+ export const StatusBar: typeof StatusBarWithStatics & { readonly currentHeight?: number } =
57
+ StatusBarWithStatics;
@@ -0,0 +1,50 @@
1
+ import '@angular/compiler';
2
+ import { Component } from '@angular/core';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import * as engine from '@symbiote-native/engine';
5
+ import { installFabric } from '@symbiote-native/test-utils';
6
+
7
+ import { mount, unmount } from '../../render';
8
+ import { StatusBar } from './index';
9
+
10
+ const ROOT_TAG = 901;
11
+ const fabric = installFabric();
12
+
13
+ @Component({
14
+ selector: 'symbiote-status-bar-host',
15
+ standalone: true,
16
+ imports: [StatusBar],
17
+ template: ` <StatusBar [barStyle]="'dark-content'" [hidden]="true" [animated]="false" /> `,
18
+ })
19
+ class StatusBarHost {}
20
+
21
+ beforeEach(() => fabric.reset());
22
+ afterEach(() => unmount(ROOT_TAG));
23
+
24
+ describe('StatusBar', () => {
25
+ it('applies status bar props on mount and renders no Fabric node', async () => {
26
+ const spy = vi.spyOn(engine, 'applyStatusBarProps').mockReturnValue(undefined);
27
+
28
+ mount(ROOT_TAG, StatusBarHost);
29
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
30
+
31
+ expect(spy).toHaveBeenCalledWith(
32
+ expect.objectContaining({
33
+ barStyle: 'dark-content',
34
+ hidden: true,
35
+ animated: false,
36
+ }),
37
+ );
38
+
39
+ const root = fabric.appRoot();
40
+ expect(root.children).toHaveLength(0);
41
+ });
42
+
43
+ it('exposes imperative statics on the component value', () => {
44
+ expect(typeof StatusBar.setHidden).toBe('function');
45
+ expect(typeof StatusBar.setBarStyle).toBe('function');
46
+ expect(typeof StatusBar.setNetworkActivityIndicatorVisible).toBe('function');
47
+ expect(typeof StatusBar.setBackgroundColor).toBe('function');
48
+ expect(typeof StatusBar.setTranslucent).toBe('function');
49
+ });
50
+ });
@@ -0,0 +1,119 @@
1
+ // Angular primitive host components over Symbiote engine intrinsics. These are the only
2
+ // components that directly own the `symbiote-*` selectors; every composed adapter
3
+ // component imports them and renders them in its template. Declaring `style` as a real
4
+ // Angular input prevents Angular's CSS style engine from decomposing RN `StyleProp` arrays.
5
+
6
+ import { Component } from '@angular/core';
7
+ import { SymbiotePrimitiveHost } from './shared';
8
+
9
+ export {
10
+ anchorHostStyle,
11
+ anchorStyleProp,
12
+ stableAnchorStyle,
13
+ SymbioteHostPropsDirective,
14
+ } from './shared';
15
+
16
+ @Component({
17
+ selector: 'symbiote-view, View',
18
+ standalone: true,
19
+ template: '<ng-content></ng-content>',
20
+ })
21
+ export class ViewHost extends SymbiotePrimitiveHost {}
22
+
23
+ @Component({
24
+ selector: 'symbiote-text, Text',
25
+ standalone: true,
26
+ template: '<ng-content></ng-content>',
27
+ })
28
+ export class TextHost extends SymbiotePrimitiveHost {}
29
+
30
+ @Component({
31
+ selector: 'symbiote-image',
32
+ standalone: true,
33
+ template: '<ng-content></ng-content>',
34
+ })
35
+ export class ImageHost extends SymbiotePrimitiveHost {}
36
+
37
+ @Component({
38
+ selector: 'symbiote-scroll-view',
39
+ standalone: true,
40
+ template: '<ng-content></ng-content>',
41
+ })
42
+ export class ScrollViewHost extends SymbiotePrimitiveHost {}
43
+
44
+ @Component({
45
+ selector: 'symbiote-scroll-content',
46
+ standalone: true,
47
+ template: '<ng-content></ng-content>',
48
+ })
49
+ export class ScrollContentView extends SymbiotePrimitiveHost {}
50
+
51
+ @Component({
52
+ selector: 'symbiote-horizontal-scroll-view',
53
+ standalone: true,
54
+ template: '<ng-content></ng-content>',
55
+ })
56
+ export class HorizontalScrollView extends SymbiotePrimitiveHost {}
57
+
58
+ @Component({
59
+ selector: 'symbiote-horizontal-scroll-content',
60
+ standalone: true,
61
+ template: '<ng-content></ng-content>',
62
+ })
63
+ export class HorizontalScrollContentView extends SymbiotePrimitiveHost {}
64
+
65
+ @Component({
66
+ selector: 'symbiote-text-input',
67
+ standalone: true,
68
+ template: '<ng-content></ng-content>',
69
+ })
70
+ export class TextInputHost extends SymbiotePrimitiveHost {}
71
+
72
+ @Component({
73
+ selector: 'symbiote-text-input-multiline',
74
+ standalone: true,
75
+ template: '<ng-content></ng-content>',
76
+ })
77
+ export class MultilineTextInputHost extends SymbiotePrimitiveHost {}
78
+
79
+ @Component({
80
+ selector: 'symbiote-switch',
81
+ standalone: true,
82
+ template: '<ng-content></ng-content>',
83
+ })
84
+ export class SwitchHost extends SymbiotePrimitiveHost {}
85
+
86
+ @Component({
87
+ selector: 'symbiote-activity-indicator',
88
+ standalone: true,
89
+ template: '<ng-content></ng-content>',
90
+ })
91
+ export class ActivityIndicatorHost extends SymbiotePrimitiveHost {}
92
+
93
+ @Component({
94
+ selector: 'symbiote-safe-area-view',
95
+ standalone: true,
96
+ template: '<ng-content></ng-content>',
97
+ })
98
+ export class SafeAreaViewHost extends SymbiotePrimitiveHost {}
99
+
100
+ @Component({
101
+ selector: 'symbiote-modal',
102
+ standalone: true,
103
+ template: '<ng-content></ng-content>',
104
+ })
105
+ export class ModalHost extends SymbiotePrimitiveHost {}
106
+
107
+ @Component({
108
+ selector: 'symbiote-refresh-control',
109
+ standalone: true,
110
+ template: '<ng-content></ng-content>',
111
+ })
112
+ export class RefreshControlHost extends SymbiotePrimitiveHost {}
113
+
114
+ @Component({
115
+ selector: 'symbiote-input-accessory-view',
116
+ standalone: true,
117
+ template: '<ng-content></ng-content>',
118
+ })
119
+ export class InputAccessoryViewHost extends SymbiotePrimitiveHost {}
@@ -0,0 +1,190 @@
1
+ import {
2
+ ChangeDetectorRef,
3
+ Directive,
4
+ ElementRef,
5
+ inject,
6
+ Input,
7
+ type OnChanges,
8
+ Renderer2,
9
+ type SimpleChanges,
10
+ } from '@angular/core';
11
+ import {
12
+ flattenStyle,
13
+ isSymbioteNode,
14
+ type IHostInstance,
15
+ type IStyleProp,
16
+ } from '@symbiote-native/engine';
17
+
18
+ /**
19
+ * A composed Angular component (Pressable, SafeAreaView, ScrollView, ...) is created as a
20
+ * non-painting ANCHOR host (`ANCHOR_HOST_COMPONENTS` in renderer.ts) so it doesn't paint an
21
+ * extra native view over its own template's real content. `class="..."`/`[class.x]`/`[ngClass]`
22
+ * at the component's use site ALWAYS resolves through the renderer's addClass/removeClass onto
23
+ * that anchor — Angular gives no `@Input()` interception hook for `class` the way it does for
24
+ * `[style]` (which is why `style` alone was declared as an `@Input()` in the first place, see
25
+ * `SymbiotePrimitiveHost` above) — so the anchor ends up holding the fully-resolved class-derived
26
+ * style (`routeProp`'s `commitClassStyle` already wrote it to `anchor.props.style`) while the
27
+ * REAL native node the component's own template creates never sees it.
28
+ *
29
+ * Call this from a composed component's own `hostProps`/style getter and merge the result AHEAD
30
+ * of its own explicit `style` @Input, e.g. `style: [anchorHostStyle(this.elementRef),
31
+ * this.resolvedStyle]` — array order matters: `flattenStyle`'s later-wins collapse is what makes
32
+ * an explicit `[style]` on the component still beat its ambient class, mirroring the
33
+ * class-loses-to-explicit-style cascade `commitClassStyle` already enforces for a direct
34
+ * primitive. `this.elementRef` must be this component's OWN injected `ElementRef` (its anchor
35
+ * host), not a `@ViewChild` into its template's inner primitive.
36
+ *
37
+ * Skipping this call for a new `ANCHOR_HOST_COMPONENTS` entry compiles clean under `tsc` AND a
38
+ * real `ngc` AOT build — no type error, no template error — it just silently drops `class="..."`
39
+ * forever; only a device/simulator render (or a committed-node style assertion) catches it.
40
+ */
41
+ export function anchorHostStyle(elementRef: ElementRef<unknown>): unknown {
42
+ const node = elementRef.nativeElement;
43
+ return isSymbioteNode(node) ? node.props.style : undefined;
44
+ }
45
+
46
+ // anchorHostStyle returns `unknown` (it reads whatever already resolved onto the anchor node's
47
+ // props bag); a style prop's fields are all optional so any object structurally satisfies it —
48
+ // this narrows just enough (falsy-per-IStyleProp, or an object/array) to fold it into a
49
+ // strictly-typed IStyleProp<T> array without an `as` cast, for a composed component whose own
50
+ // inner primitive declares a real `@Input() style: IStyleProp<SomeConcreteStyle>` (e.g.
51
+ // TouchableOpacity's Pressable, Button's TouchableOpacity) rather than the loosely-typed
52
+ // `symbioteHostProps` bag most composed components merge anchorHostStyle into directly.
53
+ function isStyleValue<T>(value: unknown): value is IStyleProp<T> {
54
+ return (
55
+ value === undefined ||
56
+ value === null ||
57
+ value === false ||
58
+ value === '' ||
59
+ typeof value === 'object'
60
+ );
61
+ }
62
+
63
+ export function anchorStyleProp<T>(elementRef: ElementRef<unknown>): IStyleProp<T> | undefined {
64
+ const value = anchorHostStyle(elementRef);
65
+ return isStyleValue<T>(value) ? value : undefined;
66
+ }
67
+
68
+ function isRecord(value: unknown): value is Record<string, unknown> {
69
+ return typeof value === 'object' && value !== null;
70
+ }
71
+
72
+ function shallowStyleEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean {
73
+ const aKeys = Object.keys(a);
74
+ if (aKeys.length !== Object.keys(b).length) return false;
75
+ return aKeys.every(key => a[key] === b[key]);
76
+ }
77
+
78
+ /**
79
+ * `anchorHostStyle` must be re-read on every check (a bare `class="x"` with no matching
80
+ * `[style]` binding never appears in Angular's `SimpleChanges`/dirty-check inputs, so nothing
81
+ * else signals "the anchor's class-derived style may have changed"). But `flattenStyle([...])`
82
+ * always allocates a FRESH object, even when its content is byte-for-byte identical to last
83
+ * time — and a windowed list component (FlatList/VirtualizedList/SectionList/
84
+ * VirtualizedSectionList) binds its resolved style into a structural-directive-bound `@Input()`
85
+ * chain where a fresh reference on an UNRELATED CD pass is mistaken for a real change, which
86
+ * reschedules ANOTHER CD tick (`VListOutletDirective`'s `context` `@Input()` calling
87
+ * `markForCheck()`) — free-running forever. See the `flat-list-array-style.test.ts` regression
88
+ * this fixes (and the near-identical `lastRecompute` dedup guard in `virtualized-list/index.ts`,
89
+ * which this mirrors at the style-merge call site instead of a whole recompute step). Returns
90
+ * `previous` unchanged when the freshly-flattened merge is shallow-equal to it, so a downstream
91
+ * `@Input() style` binding sees NO change and skips its own update on an unrelated tick.
92
+ */
93
+ export function stableAnchorStyle(
94
+ elementRef: ElementRef<unknown>,
95
+ explicitStyle: unknown,
96
+ previous: unknown,
97
+ ): Record<string, unknown> {
98
+ const next = flattenStyle([anchorHostStyle(elementRef), explicitStyle]);
99
+ return isRecord(previous) && shallowStyleEqual(previous, next) ? previous : next;
100
+ }
101
+
102
+ const ON_PREFIX = /^on[A-Z]/;
103
+
104
+ /**
105
+ * Base for all Angular primitive host components (`symbiote-view`, `symbiote-text`, ...).
106
+ * RN's `StyleProp` can be an object, an array, nested arrays, falsy entries, and Animated
107
+ * values. Angular's raw `[style]` binding on a custom element routes through the CSS style
108
+ * engine (`setStyle` per key), which crashes on numeric array keys. By declaring `style` as
109
+ * an Angular `@Input()` on a known component, we intercept it here and forward a flattened
110
+ * style object through `Renderer2.setProperty`, bypassing Angular's CSS styling path.
111
+ *
112
+ * Other props (source, pointerEvents, ...) are intentionally *not* declared as inputs.
113
+ * Angular then treats them as host property bindings and still dispatches them through the
114
+ * custom renderer's `setProperty`, so the engine's `routeProp` handles them as usual.
115
+ */
116
+ @Directive()
117
+ export abstract class SymbiotePrimitiveHost implements OnChanges {
118
+ private readonly renderer = inject(Renderer2);
119
+ private readonly elementRef = inject(ElementRef);
120
+
121
+ @Input() style?: IStyleProp<unknown>;
122
+
123
+ /**
124
+ * The underlying engine host node (public instance). Exposed so `findNodeHandle` can resolve
125
+ * an Angular template ref (`#ref`) on a primitive host to its native reactTag.
126
+ */
127
+ get nativeElement(): IHostInstance {
128
+ return this.elementRef.nativeElement;
129
+ }
130
+
131
+ ngOnChanges(changes: SimpleChanges): void {
132
+ if (!('style' in changes)) return;
133
+ const value = this.style === undefined ? undefined : flattenStyle(this.style);
134
+ this.renderer.setProperty(this.elementRef.nativeElement, 'style', value);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Applies a whole resolved props record (a shared `renderX(...).props` bag) onto a host
140
+ * element via `Renderer2.setProperty`, one call per key. Angular templates compile bindings
141
+ * ahead of time — there is no `v-bind="obj"` equivalent for a custom element — so a component
142
+ * whose props come from a shared render function would otherwise have to declare every key as
143
+ * its own static `[prop]="hostProps['x']"` binding. This generalizes the pattern
144
+ * `SwitchHostPropsDirective` proved first: `<symbiote-x [symbioteHostProps]="hostProps" #ref="symbioteHost">`,
145
+ * with `ref.node` exposing the native element for any imperative follow-up call.
146
+ */
147
+ @Directive({
148
+ selector: '[symbioteHostProps]',
149
+ standalone: true,
150
+ exportAs: 'symbioteHost',
151
+ inputs: ['symbioteHostProps'],
152
+ })
153
+ export class SymbioteHostPropsDirective {
154
+ private readonly renderer = inject(Renderer2);
155
+ private readonly elementRef = inject<ElementRef<unknown>>(ElementRef);
156
+ // The directive lives in its HOST component's template, so its injected ChangeDetectorRef IS
157
+ // that component's own view detector — the one thing that can refresh it (see wrapCallback).
158
+ private readonly cdr = inject(ChangeDetectorRef);
159
+
160
+ get node(): unknown {
161
+ return this.elementRef.nativeElement;
162
+ }
163
+
164
+ set symbioteHostProps(props: Record<string, unknown>) {
165
+ for (const [key, value] of Object.entries(props)) {
166
+ this.renderer.setProperty(this.elementRef.nativeElement, key, this.wrapCallback(key, value));
167
+ }
168
+ }
169
+
170
+ // A flat-bag `onX` callback (responder negotiation, onLongPress, …) is invoked by the engine's
171
+ // event dispatch — a plain JS call, entirely outside Angular. In Angular 20 a component compiles
172
+ // as a SignalView (not CheckAlways), so a plain (non-signal) state mutation inside such a callback
173
+ // does NOT dirty the view's reactive consumer, and the scheduler's root detectChanges() will NOT
174
+ // descend into it (a Global pass only refreshes CheckAlways / Dirty / RefreshView views) — the
175
+ // "pan does nothing" bug on a nested component. `markForCheck()` flags THIS component's view AND
176
+ // all its ancestors with RefreshView (which survives the descent) and notifies the CD scheduler,
177
+ // so the next root tick repaints the mutation. It targets the directive's injecting view
178
+ // (`_cdRefInjectingView`, i.e. the host component), unlike `detectChanges()` which acts on a
179
+ // resolved `_lView` that is NOT the host component here. The Angular twin of what React/Vue get
180
+ // for free (setState / proxy reactivity).
181
+ private wrapCallback(key: string, value: unknown): unknown {
182
+ if (!ON_PREFIX.test(key) || typeof value !== 'function') return value;
183
+ const handler = value as (...args: unknown[]) => unknown;
184
+ return (...args: unknown[]): unknown => {
185
+ const result = handler(...args);
186
+ this.cdr.markForCheck();
187
+ return result;
188
+ };
189
+ }
190
+ }
@@ -0,0 +1,235 @@
1
+ // Mount an Angular app onto a Fabric surface. The native host hands us a rootTag; we
2
+ // create a surface and bootstrap a standalone Angular component WITHOUT platform-browser
3
+ // (no DOM) — createEnvironmentInjector + createComponent over @angular/core only, with our
4
+ // SymbioteRendererFactory provided so Angular drives the engine, which commits into
5
+ // nativeFabricUIManager; RN's own renderer never in the path. Angular twin of
6
+ // adapters/vue/src/render.ts.
7
+
8
+ import {
9
+ createElement as createEngineElement,
10
+ createSurface,
11
+ disposeRoot,
12
+ dlog,
13
+ toPublicInstance,
14
+ type ISymbioteNode,
15
+ type IRootTag,
16
+ type SymbioteSurface,
17
+ } from '@symbiote-native/engine';
18
+ import { descriptorFor } from '@symbiote-native/components';
19
+ import {
20
+ ApplicationRef,
21
+ createComponent,
22
+ createEnvironmentInjector,
23
+ DOCUMENT,
24
+ ErrorHandler,
25
+ RendererFactory2,
26
+ ɵINJECTOR_SCOPE as INJECTOR_SCOPE,
27
+ ɵprovideZonelessChangeDetectionInternal as provideZonelessChangeDetectionInternal,
28
+ type ComponentRef,
29
+ type EnvironmentInjector,
30
+ type Type,
31
+ } from '@angular/core';
32
+ import { SymbioteRendererFactory } from '../renderer';
33
+ import { ColorSchemeService, WindowDimensionsService } from '../services';
34
+
35
+ // The two FFI-edge casts of this adapter, both confined here to bootstrap. Angular's core
36
+ // types model a browser: the injector parent is a non-null EnvironmentInjector, the host is
37
+ // a DOM Element. Our root has neither shape — a null parent IS a root injector, and the host
38
+ // is a SymbioteSurface (or, for a wrapped AppRegistry root, a plain engine node) the
39
+ // SymbioteRenderer (not the DOM) consumes. These are the sanctioned I/O-edge casts where our
40
+ // objects cross into Angular's web-typed API; revisit if Angular ever exposes a cast-free
41
+ // host. Nothing else in the adapter casts.
42
+ function rootInjectorParent(): EnvironmentInjector {
43
+ return null as unknown as EnvironmentInjector;
44
+ }
45
+ function asAngularHost(hostNode: SymbioteSurface | ISymbioteNode): Element {
46
+ // Angular's locateHostElement reads hostElement.tagName even when a concrete hostElement
47
+ // is supplied. The host container is given Angular the tiny DOM-shaped field it probes
48
+ // without changing the renderer target.
49
+ Object.defineProperty(hostNode, 'tagName', {
50
+ configurable: true,
51
+ value: 'symbiote-root',
52
+ });
53
+ return hostNode as unknown as Element;
54
+ }
55
+
56
+ // A bare `symbiote-view` node, created directly through the engine (bypassing Angular's own
57
+ // createElement, which only resolves KNOWN symbiote primitives, never an arbitrary component's
58
+ // selector — see SymbioteRenderer.createElement). Used as the AppRegistry root's host when a
59
+ // wrapperComponentProvider is set: Angular has no hostElement-less bootstrap for our renderer
60
+ // (it would try `renderer.createElement(rootComponent.selector)`, which throws for anything
61
+ // that isn't one of our primitives), so the root needs an explicit host too, one we then hand
62
+ // to the wrapper as projectable content.
63
+ function createDetachedViewHost(): ISymbioteNode {
64
+ const descriptor = descriptorFor('View');
65
+ return toPublicInstance(createEngineElement(descriptor.component, descriptor.isText));
66
+ }
67
+
68
+ interface IMountedApp {
69
+ cmpRef: ComponentRef<unknown>;
70
+ rootRef: ComponentRef<unknown> | undefined;
71
+ injector: EnvironmentInjector;
72
+ }
73
+
74
+ // AppRegistry's `wrapperComponentProvider` support: the root renders detached (Angular gives
75
+ // it its own host node via our renderer, not yet attached anywhere), and the wrapper — the
76
+ // component actually attached to the surface — receives that host node as a projected child.
77
+ // `<ng-content>` in the wrapper's template is the Angular idiom for "render my children", the
78
+ // direct twin of React's `createElement(Wrapper, null, rootElement)`.
79
+ export interface IMountOptions {
80
+ initialProps?: object;
81
+ wrapperComponent?: Type<unknown>;
82
+ }
83
+
84
+ function applyInputs(cmpRef: ComponentRef<unknown>, initialProps: object | undefined): void {
85
+ if (initialProps === undefined) return;
86
+ for (const [key, value] of Object.entries(initialProps)) {
87
+ cmpRef.setInput(key, value);
88
+ }
89
+ }
90
+
91
+ // One Angular app per surface, so a surface can be torn down (unmount) or cleanly
92
+ // re-mounted on the same rootTag: the bridgeless host stops and restarts a surface on Fast
93
+ // Refresh and on lifecycle/focus changes, reusing the rootTag.
94
+ const apps = new Map<IRootTag, IMountedApp>();
95
+
96
+ function teardown(rootTag: IRootTag): void {
97
+ const app = apps.get(rootTag);
98
+ if (app === undefined) return;
99
+ app.rootRef?.destroy();
100
+ app.cmpRef.destroy();
101
+ app.injector.destroy();
102
+ apps.delete(rootTag);
103
+ disposeRoot(rootTag);
104
+ }
105
+
106
+ export function mount(
107
+ rootTag: IRootTag,
108
+ rootComponent: Type<unknown>,
109
+ options?: IMountOptions,
110
+ ): SymbioteSurface {
111
+ // A re-mount on a live rootTag starts clean; otherwise the stale app double-drives the surface.
112
+ teardown(rootTag);
113
+
114
+ const surface = createSurface(rootTag);
115
+ const injector = createEnvironmentInjector(
116
+ [
117
+ {
118
+ provide: RendererFactory2,
119
+ useValue: new SymbioteRendererFactory(surface),
120
+ },
121
+ { provide: DOCUMENT, useValue: { head: surface, body: surface } },
122
+ // createEnvironmentInjector with a null parent gives this injector scope
123
+ // {'environment'} only (see EnvironmentNgModuleRefAdapter), so providedIn:'root'
124
+ // tokens — ApplicationRef included — never resolve on their own (R3Injector.get
125
+ // walks up looking for an injector whose `scopes` contains 'root', and a null
126
+ // parent means that search always dead-ends in NullInjector). platform-browser's
127
+ // BROWSER_MODULE_PROVIDERS solves this the same way for real DOM apps: it hands
128
+ // { provide: INJECTOR_SCOPE, useValue: 'root' } to its OWN app-level providers, and
129
+ // R3Injector's constructor reads that provider and self-tags this.scopes with
130
+ // 'root'. We do the same here — no PlatformRef, no DOM, just this one provider.
131
+ { provide: INJECTOR_SCOPE, useValue: 'root' },
132
+ // Supplies the real ChangeDetectionSchedulerImpl (microtask-batched, self-scheduling
133
+ // via ApplicationRef.afterTick) + NoopNgZone + ZONELESS_ENABLED: true — the exact
134
+ // bundle internalCreateApplication() uses. With ApplicationRef reachable (see above),
135
+ // there is no more reason for a hand-rolled scheduler: it replaces our old
136
+ // unconditional `rootView.detectChanges(); cmpView.detectChanges()` (which force-ran
137
+ // BOTH root views on every tick regardless of cause) with Angular's own tick(), which
138
+ // only enters an attached view when something actually marked it (RefreshView / Dirty
139
+ // consumer / HasChildViewsToRefresh). NOTE: this does NOT stop the root's own template
140
+ // from re-running on a plain press or `ChangeDetectorRef.markForCheck()` anywhere in
141
+ // the tree — `markViewDirty` (which both native (event) bindings and `markForCheck()`
142
+ // go through, see SymbioteHostPropsDirective) unconditionally sets RefreshView on
143
+ // EVERY ancestor up to the root; that is fundamental, unavoidable Angular zoneless
144
+ // behavior, true in every Angular app, not something this swap changes. What DOES
145
+ // still protect a sibling branch from an unrelated press is a genuine child
146
+ // `@Component` boundary — SignalView-compiled children are skip-eligible regardless of
147
+ // this scheduler.
148
+ ...provideZonelessChangeDetectionInternal(),
149
+ // Angular's own INTERNAL_APPLICATION_ERROR_HANDLER (core.mjs) reports a tick()
150
+ // exception by calling `injector.get(ErrorHandler)` — a normal `bootstrapApplication`
151
+ // registers this token by default (platform-browser's BROWSER_MODULE_PROVIDERS), but
152
+ // our from-scratch environment injector never did, so that lookup itself threw
153
+ // NG0201 and REPLACED whatever the real error was with an unrelated "No provider
154
+ // found for ErrorHandler" — the real exception never got logged, and the NG0201 itself
155
+ // propagated out of a bare Timeout callback uncaught (nothing above it in the stack to
156
+ // catch it), i.e. any async tick() exception, anywhere in the app, crashed hard instead
157
+ // of being reported. Providing the default `ErrorHandler` (same one bootstrapApplication
158
+ // ships) restores the intended behavior: `console.error('ERROR', e)` and keep running.
159
+ { provide: ErrorHandler, useClass: ErrorHandler },
160
+ ColorSchemeService,
161
+ WindowDimensionsService,
162
+ ],
163
+ rootInjectorParent(),
164
+ );
165
+
166
+ // hostElement = the surface: the component's template content commits straight into the
167
+ // surface with no wrapper view, the engine wrapping surface.children in its synthetic flex
168
+ // root — the Angular equivalent of Vue's `app.mount(surface)`.
169
+ let cmpRef: ComponentRef<unknown>;
170
+ let rootRef: ComponentRef<unknown> | undefined;
171
+ if (options?.wrapperComponent === undefined) {
172
+ cmpRef = createComponent<unknown>(rootComponent, {
173
+ environmentInjector: injector,
174
+ hostElement: asAngularHost(surface),
175
+ });
176
+ applyInputs(cmpRef, options?.initialProps);
177
+ } else {
178
+ // The root gets its own host node from the renderer (createComponent without a
179
+ // hostElement) but is not attached to the surface directly; it is handed to the
180
+ // wrapper as projectable content instead, so only the wrapper needs a real host.
181
+ const rootHost = asAngularHost(createDetachedViewHost());
182
+ rootRef = createComponent<unknown>(rootComponent, {
183
+ environmentInjector: injector,
184
+ hostElement: rootHost,
185
+ });
186
+ applyInputs(rootRef, options.initialProps);
187
+ cmpRef = createComponent<unknown>(options.wrapperComponent, {
188
+ environmentInjector: injector,
189
+ hostElement: asAngularHost(surface),
190
+ projectableNodes: [[rootHost]],
191
+ });
192
+ }
193
+
194
+ // Attach both root views to ApplicationRef so its own tick() (via the real
195
+ // ChangeDetectionSchedulerImpl provided above) drives them from here on — no manual
196
+ // ChangeDetectorRef juggling needed. markForCheck anywhere in the tree now notifies
197
+ // the real scheduler, which batches a microtask and calls appRef.tick() itself.
198
+ const appRef = injector.get(ApplicationRef);
199
+ appRef.attachView(cmpRef.hostView);
200
+ if (rootRef !== undefined) {
201
+ appRef.attachView(rootRef.hostView);
202
+ }
203
+
204
+ dlog(`angular mount root=${rootTag}`);
205
+ appRef.tick(); // first paint
206
+ surface.requestCommit();
207
+
208
+ apps.set(rootTag, { cmpRef, rootRef, injector });
209
+ return surface;
210
+ }
211
+
212
+ // Tear down a surface by rootTag: the public pair of `mount`, and the JS half of the
213
+ // bridgeless `RN$stopSurface` contract (see installStopSurfaceGlobal).
214
+ export function unmount(rootTag: IRootTag): void {
215
+ dlog(`angular unmount root=${rootTag}`);
216
+ teardown(rootTag);
217
+ }
218
+
219
+ // `global.RN$stopSurface` is the JSI hook C++ AppRegistryBinding::stopSurface calls to stop a
220
+ // Fabric surface. RN installs it from its own renderer; symbiote REPLACES that renderer, so
221
+ // without this the binding throws "Global was not installed" on every surface stop (Fast
222
+ // Refresh, focus/lifecycle) and the screen goes blank. Same contract as the React/Vue
223
+ // adapters: an app uses one adapter, so exactly one installer runs.
224
+ declare global {
225
+ var RN$stopSurface: ((surfaceId: number) => void) | undefined;
226
+ }
227
+
228
+ function installStopSurfaceGlobal(): void {
229
+ globalThis.RN$stopSurface = (surfaceId: number): void => {
230
+ unmount(surfaceId);
231
+ };
232
+ dlog('installed global.RN$stopSurface');
233
+ }
234
+
235
+ installStopSurfaceGlobal();