@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,147 @@
1
+ // Integration coverage: run the real Angular runtime (createComponent + RendererFactory2)
2
+ // over the headless fake Fabric slot. Unlike renderer.test.ts, this proves mount() wires
3
+ // Angular's bootstrap to the Symbiote renderer, so a standalone template paints and updates
4
+ // through the engine.
5
+
6
+ import '@angular/compiler';
7
+ import { Component } from '@angular/core';
8
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
9
+ import { installFabric } from '@symbiote-native/test-utils';
10
+ import { mount, unmount } from './index';
11
+
12
+ const ROOT_TAG = 808;
13
+
14
+ const fabric = installFabric();
15
+ const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
16
+ const drainAngularAndCommit = async (): Promise<void> => {
17
+ await tick();
18
+ await tick();
19
+ };
20
+
21
+ class TestView {}
22
+ Component({
23
+ selector: 'symbiote-view',
24
+ standalone: true,
25
+ template: '<ng-content></ng-content>',
26
+ })(TestView);
27
+
28
+ class TestText {}
29
+ Component({
30
+ selector: 'symbiote-text',
31
+ standalone: true,
32
+ template: '<ng-content></ng-content>',
33
+ })(TestText);
34
+
35
+ class SmokeComponent {
36
+ name = 'Angular';
37
+ count = 0;
38
+ boxStyle = { padding: 12 };
39
+
40
+ increment(): void {
41
+ this.count += 1;
42
+ }
43
+ }
44
+
45
+ Component({
46
+ selector: 'symbiote-angular-smoke',
47
+ standalone: true,
48
+ imports: [TestView, TestText],
49
+ template: `<symbiote-view [style]="boxStyle"><symbiote-text>Hello {{ name }}</symbiote-text><symbiote-view testID="counter" (press)="increment()"><symbiote-text>tapped {{ count }}×</symbiote-text></symbiote-view></symbiote-view>`,
50
+ })(SmokeComponent);
51
+
52
+ beforeEach(() => fabric.reset());
53
+ afterEach(() => unmount(ROOT_TAG));
54
+
55
+ // A plain (non-OnPush) child component compiles as SignalView in Angular 20+, so an UNTOUCHED
56
+ // sibling child is skipped by the per-view CheckAlways/Dirty/RefreshView gate inside
57
+ // detectChangesInView — true with or without ApplicationRef (the gate lives inside
58
+ // detectChangesInView regardless of who calls it). This is NOT a claim that the root's own
59
+ // template is spared: markViewDirty (used by both native (event) bindings and
60
+ // ChangeDetectorRef.markForCheck(), see SymbioteHostPropsDirective) unconditionally sets
61
+ // RefreshView on every ancestor up to the root — a press anywhere always re-runs the root's
62
+ // own template, in every Angular app, regardless of scheduler. What this guards is narrower
63
+ // but still real: a SIBLING component with no dirty descendant of its own must not be dragged
64
+ // along for the ride.
65
+ let unrelatedRenderCount = 0;
66
+ class CounterChild {
67
+ count = 0;
68
+
69
+ increment(): void {
70
+ this.count += 1;
71
+ }
72
+ }
73
+ Component({
74
+ selector: 'counter-child',
75
+ standalone: true,
76
+ imports: [TestView, TestText],
77
+ template:
78
+ '<symbiote-view testID="counter" (press)="increment()"><symbiote-text>tapped {{ count }}×</symbiote-text></symbiote-view>',
79
+ })(CounterChild);
80
+
81
+ class UnrelatedSiblingChild {
82
+ trackRender(): number {
83
+ unrelatedRenderCount++;
84
+ return unrelatedRenderCount;
85
+ }
86
+ }
87
+ Component({
88
+ selector: 'unrelated-sibling-child',
89
+ standalone: true,
90
+ template: '<symbiote-text>unrelated {{ trackRender() }}</symbiote-text>',
91
+ })(UnrelatedSiblingChild);
92
+
93
+ class TargetedComponent {}
94
+ Component({
95
+ selector: 'symbiote-angular-targeted',
96
+ standalone: true,
97
+ imports: [TestView, CounterChild, UnrelatedSiblingChild],
98
+ template: `<symbiote-view><counter-child /><unrelated-sibling-child /></symbiote-view>`,
99
+ })(TargetedComponent);
100
+
101
+ describe('Angular mount', () => {
102
+ it('bootstraps a standalone component into a committed Fabric tree', async () => {
103
+ mount(ROOT_TAG, SmokeComponent);
104
+ await tick();
105
+
106
+ const root = fabric.appRoot();
107
+ expect(fabric.serialize(root.children)).toBe(
108
+ 'RCTView(RCTText(RCTRawText "Hello Angular")RCTView(RCTText(RCTRawText "tapped 0×")))',
109
+ );
110
+ expect(root.children[0]?.props).toMatchObject({ padding: 12 });
111
+ });
112
+
113
+ it('runs Angular change detection after a native press and recommits text', async () => {
114
+ mount(ROOT_TAG, SmokeComponent);
115
+ await tick();
116
+
117
+ const counter = fabric.find(node => node.props.testID === 'counter');
118
+ expect(counter).toBeDefined();
119
+
120
+ fabric.fireEvent(counter?.instanceHandle, 'topTouchStart');
121
+ fabric.fireEvent(counter?.instanceHandle, 'topTouchEnd');
122
+ await drainAngularAndCommit();
123
+
124
+ expect(fabric.serialize(fabric.appRoot().children)).toContain('RCTRawText "tapped 1×"');
125
+ });
126
+
127
+ it('does not re-check a sibling child component on a press inside a different child', async () => {
128
+ unrelatedRenderCount = 0;
129
+ mount(ROOT_TAG, TargetedComponent);
130
+ await tick();
131
+
132
+ const afterFirstPaint = unrelatedRenderCount;
133
+ expect(afterFirstPaint).toBeGreaterThan(0);
134
+
135
+ const counter = fabric.find(node => node.props.testID === 'counter');
136
+ fabric.fireEvent(counter?.instanceHandle, 'topTouchStart');
137
+ fabric.fireEvent(counter?.instanceHandle, 'topTouchEnd');
138
+ await drainAngularAndCommit();
139
+
140
+ expect(fabric.serialize(fabric.appRoot().children)).toContain('RCTRawText "tapped 1×"');
141
+ // UnrelatedSiblingChild has no dirty descendant of its own, so it must not be re-checked
142
+ // just because CounterChild (a completely separate branch) got pressed. This does NOT prove
143
+ // the root's own template stays untouched — the root's template still re-runs on every
144
+ // press (see render.ts).
145
+ expect(unrelatedRenderCount).toBe(afterFirstPaint);
146
+ });
147
+ });
@@ -0,0 +1,347 @@
1
+ // An Angular custom renderer over @symbiote-native/engine. Angular components never touch the
2
+ // DOM directly — every paint goes through Renderer2 (created per-component by
3
+ // RendererFactory2). We provide OUR factory, so each Renderer2 method maps onto the
4
+ // engine's tiny mutation API; the engine owns all Fabric clone-on-write, shared with
5
+ // every other adapter. This is the Angular twin of adapters/vue/src/renderer.ts — proof
6
+ // that the same engine mutation API drives both frameworks.
7
+
8
+ import {
9
+ appendChild,
10
+ createAnchor,
11
+ createElement,
12
+ createRawText,
13
+ dlog,
14
+ getExplicitStyle,
15
+ insertBefore,
16
+ isDebug,
17
+ isSymbioteNode,
18
+ removeChild,
19
+ routeProp,
20
+ setEventListener,
21
+ setText,
22
+ toPublicInstance,
23
+ RAW_TEXT_COMPONENT,
24
+ SymbioteSurface,
25
+ type ISymbioteNode,
26
+ } from '@symbiote-native/engine';
27
+ import { descriptorFor } from '@symbiote-native/components';
28
+ import type { Renderer2, RendererFactory2, RendererType2 } from '@angular/core';
29
+ import { isAnchorHostComponent } from '../anchor-host-registry';
30
+ import {
31
+ getScrollViewProjection,
32
+ removeScrollViewProjectedChild,
33
+ } from '../components/scroll-view/projection';
34
+
35
+ // Angular host nodes are all SymbioteNode (elements, raw text, anchors). The mount
36
+ // container is the surface, so a parent can be either a node or the surface root.
37
+ type IHostNode = ISymbioteNode;
38
+ type IHostElement = ISymbioteNode | SymbioteSurface;
39
+
40
+ function isSurface(parent: IHostElement): parent is SymbioteSurface {
41
+ return parent instanceof SymbioteSurface;
42
+ }
43
+
44
+ function isRawText(node: ISymbioteNode): boolean {
45
+ return node.component === RAW_TEXT_COMPONENT;
46
+ }
47
+
48
+ function isRecord(value: unknown): value is Record<string, unknown> {
49
+ return typeof value === 'object' && value !== null;
50
+ }
51
+
52
+ // Diagnostic-only: tags each anchor with a sequential id so log lines can tell distinct
53
+ // anchors apart (they all otherwise print the same generic '#anchor' component name).
54
+ // Gated behind isDebug() so a production build pays zero cost (no WeakMap writes).
55
+ let anchorDebugCounter = 0;
56
+ const anchorDebugIds = new WeakMap<ISymbioteNode, number>();
57
+ function tagAnchorForDebug(node: ISymbioteNode): ISymbioteNode {
58
+ if (isDebug()) {
59
+ anchorDebugCounter += 1;
60
+ anchorDebugIds.set(node, anchorDebugCounter);
61
+ }
62
+ return node;
63
+ }
64
+
65
+ // Diagnostic identity string for a renderer node/surface — component name, or 'surface'.
66
+ // Null-tolerant: a diagnostic must never throw (Angular hands insertBefore a null refChild).
67
+ function describeHost(node: IHostElement | null | undefined): string {
68
+ if (node === null || node === undefined) return 'null';
69
+ if (isSurface(node)) return 'surface';
70
+ const anchorId = anchorDebugIds.get(node);
71
+ return anchorId !== undefined ? `${node.component}#${anchorId}` : node.component;
72
+ }
73
+
74
+ const PRIMITIVE_SELECTOR_ALIAS: Record<string, string> = {
75
+ // Public ergonomic selectors map directly to the engine primitive descriptors.
76
+ View: 'symbiote-view',
77
+ Text: 'symbiote-text',
78
+ };
79
+
80
+ // The ANCHOR_HOST_COMPONENTS Set and registerComposedComponent now live in the dependency-free
81
+ // leaf module ../anchor-host-registry (imported above as isAnchorHostComponent) — see its header
82
+ // for why the registry must NOT sit in this require-cyclic renderer module.
83
+
84
+ // Inserting a bare raw-text node anywhere but inside a <Text> is invalid in Fabric (a
85
+ // stray RCTRawText would paint). Angular's ɵɵtext only ever lands text inside a <Text>,
86
+ // but guard anyway for parity with the Vue adapter and to fail loudly on a bad template.
87
+ function assertTextPlacement(child: ISymbioteNode, parent: IHostElement): void {
88
+ if (isRawText(child) && (isSurface(parent) || !parent.isText)) {
89
+ throw new Error(`Text string "${String(child.props.text)}" must be rendered inside a <Text>`);
90
+ }
91
+ }
92
+
93
+ // One renderer per mounted surface. Every mutation asks the surface to (microtask-
94
+ // coalesced) recommit — the same seam Vue uses; a burst of Angular change-detection
95
+ // mutations collapses into one completeRoot.
96
+ export class SymbioteRenderer implements Renderer2 {
97
+ readonly data: Record<string, unknown> = {};
98
+ // Angular calls destroyNode per-node only when this is non-null; teardown happens in
99
+ // render.ts (unmount), so per-node cleanup is a no-op.
100
+ destroyNode: ((node: ISymbioteNode) => void) | null = null;
101
+
102
+ constructor(private readonly surface: SymbioteSurface) {}
103
+
104
+ destroy(): void {}
105
+
106
+ createElement(name: string): IHostNode {
107
+ // `name` is the component's host tag — a symbiote intrinsic (`symbiote-view`,
108
+ // `symbiote-text`, …), a public ergonomic alias (`View`, `Text`), or a raw Fabric view
109
+ // name for a native leaf. Public aliases are normalized to their engine primitive name
110
+ // before descriptor lookup. descriptorFor resolves it; an unknown `symbiote-*` is a typo,
111
+ // any other string flows through as a raw Fabric name (events/processors derived from its
112
+ // ViewConfig). toPublicInstance grafts the imperative API (measure / setNativeProps /
113
+ // focus) onto the raw node in place, returning the SAME identity the commit mirror keys on.
114
+ const engineName = PRIMITIVE_SELECTOR_ALIAS[name] ?? name;
115
+ if (isAnchorHostComponent(engineName)) {
116
+ const anchor = tagAnchorForDebug(createAnchor());
117
+ dlog(`angular createElement ${name} -> anchor host ${describeHost(anchor)}`);
118
+ return anchor;
119
+ }
120
+
121
+ const descriptor = descriptorFor(engineName);
122
+ const node = createElement(descriptor.component, descriptor.isText);
123
+ dlog(`angular createElement ${name} -> ${descriptor.component}`);
124
+ return toPublicInstance(node);
125
+ }
126
+
127
+ createComment(): IHostNode {
128
+ // Angular structural directives (*ngIf / @if / @for) need anchor nodes to track
129
+ // position. A real retained node the commit walk SKIPS — no native view. Twin of the
130
+ // Vue createComment path.
131
+ const anchor = tagAnchorForDebug(createAnchor());
132
+ dlog(`Angular renderer createComment -> ${describeHost(anchor)}`);
133
+ return anchor;
134
+ }
135
+
136
+ createText(value: string): IHostNode {
137
+ return createRawText(value);
138
+ }
139
+
140
+ appendChild(parent: IHostElement | null, newChild: IHostNode): void {
141
+ // Angular defers insertion for content awaiting its host component's own projection
142
+ // (see parentNode below) — mirrors Angular's own `if (parentRNode !== null)` guard in
143
+ // addLViewToLContainer: skip silently now, the later projection pass places it correctly.
144
+ if (parent === null) return;
145
+ assertTextPlacement(newChild, parent);
146
+ if (isSurface(parent)) {
147
+ dlog(`Angular renderer appendChild parent=surface child=${describeHost(newChild)}`);
148
+ parent.appendChild(newChild);
149
+ } else {
150
+ const projection = getScrollViewProjection(parent);
151
+ dlog(
152
+ `Angular renderer appendChild parent=${describeHost(parent)} child=${describeHost(newChild)} projection=${projection !== undefined}`,
153
+ );
154
+ if (projection !== undefined) {
155
+ projection.appendProjectedChild(parent, newChild, (target, child) =>
156
+ appendChild(target, child),
157
+ );
158
+ } else {
159
+ appendChild(parent, newChild);
160
+ }
161
+ }
162
+ this.surface.requestCommit();
163
+ }
164
+
165
+ insertBefore(parent: IHostElement | null, newChild: IHostNode, refChild: IHostNode | null): void {
166
+ if (parent === null) return; // see the appendChild guard above
167
+ assertTextPlacement(newChild, parent);
168
+ if (isSurface(parent)) {
169
+ dlog(
170
+ `Angular renderer insertBefore parent=surface child=${describeHost(newChild)} ref=${refChild ? describeHost(refChild) : 'null'}`,
171
+ );
172
+ if (refChild) parent.insertBefore(newChild, refChild);
173
+ else parent.appendChild(newChild);
174
+ } else {
175
+ const projection = getScrollViewProjection(parent);
176
+ dlog(
177
+ `Angular renderer insertBefore parent=${describeHost(parent)} child=${describeHost(newChild)} ref=${refChild ? describeHost(refChild) : 'null'} projection=${projection !== undefined}`,
178
+ );
179
+ if (projection !== undefined) {
180
+ projection.insertProjectedChild(parent, newChild, refChild, (target, child, before) => {
181
+ if (before === undefined) appendChild(target, child);
182
+ else insertBefore(target, child, before);
183
+ });
184
+ } else if (refChild) {
185
+ insertBefore(parent, newChild, refChild);
186
+ } else {
187
+ appendChild(parent, newChild);
188
+ }
189
+ }
190
+ this.surface.requestCommit();
191
+ }
192
+
193
+ removeChild(_parent: IHostElement | null, oldChild: IHostNode): void {
194
+ // Detach from the child's own retained parent (a top-level node lives in
195
+ // surface.children with no parent). Angular's `parent` arg is ignored in favor of the
196
+ // authoritative link, mirroring the Vue adapter's remove.
197
+ const angularParent = _parent !== null ? describeHost(_parent) : 'null';
198
+ const retainedParent = oldChild.parent !== undefined ? describeHost(oldChild.parent) : 'none';
199
+ const wasProjected = removeScrollViewProjectedChild(oldChild, (parent, child) =>
200
+ removeChild(parent, child),
201
+ );
202
+ dlog(
203
+ `Angular renderer removeChild angularParent=${angularParent} retainedParent=${retainedParent} child=${describeHost(oldChild)} viaProjection=${wasProjected}`,
204
+ );
205
+ if (!wasProjected) {
206
+ const parent = oldChild.parent;
207
+ if (parent !== undefined) removeChild(parent, oldChild);
208
+ else this.surface.removeChild(oldChild);
209
+ }
210
+ this.surface.requestCommit();
211
+ }
212
+
213
+ // FlatList/VirtualizedList cells are content projected into a component host (our
214
+ // ANCHOR_HOST_COMPONENTS, e.g. ScrollView) — Angular's own addLViewToLContainer
215
+ // (.vendors/angular node_manipulation.ts/container.ts) treats a null parent here as "defer —
216
+ // the child component's own <ng-content>/ɵɵprojection will place this once its structure
217
+ // resolves" (e.g. ScrollView's `@if(isHorizontal)` branch). Renderer2's contract types this
218
+ // return as nullable for exactly that reason; returning `this.surface` as a non-null fallback
219
+ // defeated that defer check and caused premature top-level insertion (2026-07: FlatList cells
220
+ // rendered outside their ScrollView).
221
+ //
222
+ // Safe only because appendChild/insertBefore above now also treat a null parent as "skip, wait
223
+ // for projection" — a second Angular call site (`insertAnchorNode`, hit whenever a directive
224
+ // does `inject(ViewContainerRef)`, e.g. VListOutletDirective) forwards this null straight into
225
+ // insertBefore without checking it; without that guard it crashed on-device.
226
+ parentNode(node: IHostNode): IHostElement | null {
227
+ return node.parent ?? null;
228
+ }
229
+
230
+ nextSibling(node: IHostNode): IHostNode | null {
231
+ const siblings = node.parent !== undefined ? node.parent.children : this.surface.children;
232
+ const index = siblings.indexOf(node);
233
+ return index >= 0 ? (siblings[index + 1] ?? null) : null;
234
+ }
235
+
236
+ // locateHostElement always routes createComponent's `hostElement` THROUGH here as
237
+ // `selectorOrNode` (Angular's own core.mjs) — it is never bypassed just because a real
238
+ // object (vs. a selector string) was given. A string only reaches us on the (unused here)
239
+ // selector-string bootstrap path, so the surface is the fallback for that case only.
240
+ selectRootElement(selectorOrNode: string | IHostElement): IHostElement {
241
+ return typeof selectorOrNode === 'string' ? this.surface : selectorOrNode;
242
+ }
243
+
244
+ setAttribute(el: IHostElement, name: string, value: string): void {
245
+ if (isSurface(el)) return;
246
+ routeProp(el, name, value);
247
+ this.surface.requestCommit();
248
+ }
249
+
250
+ removeAttribute(el: IHostElement, name: string): void {
251
+ if (isSurface(el)) return;
252
+ routeProp(el, name, undefined);
253
+ this.surface.requestCommit();
254
+ }
255
+
256
+ // Ivy compiles every class= / [class.foo] / [ngClass] form down to per-token addClass/
257
+ // removeClass calls (never a single setAttribute('class', ...) call), so a per-node token set
258
+ // is accumulated here and re-joined into one string on every change, then handed to
259
+ // routeProp('class', ...) exactly like Vue's template `class="..."` and React's JSX
260
+ // `className="..."` — all three resolve through the SAME centralized class+style merge in
261
+ // core/engine/src/node.ts, so a class registered via the SFC/CSS-Modules style compiler
262
+ // resolves identically regardless of adapter.
263
+ private readonly classTokens = new WeakMap<IHostNode, Set<string>>();
264
+
265
+ addClass(el: IHostElement, name: string): void {
266
+ if (isSurface(el)) return;
267
+ const tokens = this.classTokens.get(el) ?? new Set<string>();
268
+ tokens.add(name);
269
+ this.classTokens.set(el, tokens);
270
+ routeProp(el, 'class', [...tokens].join(' '));
271
+ this.surface.requestCommit();
272
+ }
273
+
274
+ removeClass(el: IHostElement, name: string): void {
275
+ if (isSurface(el)) return;
276
+ const tokens = this.classTokens.get(el);
277
+ if (tokens === undefined) return;
278
+ tokens.delete(name);
279
+ routeProp(el, 'class', tokens.size > 0 ? [...tokens].join(' ') : undefined);
280
+ this.surface.requestCommit();
281
+ }
282
+
283
+ // Angular decomposes a [style] binding into per-key setStyle calls (ɵɵstyleMap). RN wants
284
+ // the whole style object as one `style` prop, so merge each key into it — onto the explicit
285
+ // style half tracked by routeProp's centralized class+style merge (core/engine/src/node.ts),
286
+ // NOT el.props.style directly: that may now be the [classStyle, explicitStyle] array the
287
+ // merge writes, and spreading an array as a record would silently produce numeric-index keys.
288
+ setStyle(el: IHostElement, style: string, value: unknown): void {
289
+ if (isSurface(el)) return;
290
+ const current = getExplicitStyle(el);
291
+ const base = isRecord(current) ? current : {};
292
+ routeProp(el, 'style', { ...base, [style]: value });
293
+ this.surface.requestCommit();
294
+ }
295
+
296
+ removeStyle(el: IHostElement, style: string): void {
297
+ if (isSurface(el)) return;
298
+ const current = getExplicitStyle(el);
299
+ if (!isRecord(current)) return;
300
+ const { [style]: _removed, ...rest } = current;
301
+ routeProp(el, 'style', rest);
302
+ this.surface.requestCommit();
303
+ }
304
+
305
+ // [prop]="x" bindings. routeProp makes the prop-vs-event decision from the node's
306
+ // ViewConfig (identical to React/Vue), so the whole flat-bag prop layer is shared.
307
+ setProperty(el: IHostElement, name: string, value: unknown): void {
308
+ if (isSurface(el)) return;
309
+ routeProp(el, name, value);
310
+ this.surface.requestCommit();
311
+ }
312
+
313
+ setValue(node: IHostNode, value: string): void {
314
+ // A useful permanent seam: text mutations are low-frequency and the one place a stale
315
+ // `{{binding}}` (a change-detection gap) shows up as "the setValue never fired".
316
+ dlog(`Angular renderer setValue "${value}" on ${describeHost(node)}`);
317
+ setText(node, value);
318
+ this.surface.requestCommit();
319
+ }
320
+
321
+ // (event)="x" bindings. Angular hands the event name EXPLICITLY (no onX->x inference),
322
+ // so we drive the engine's structural event channel directly — the path setEventListener
323
+ // in core/engine/src/node.ts already names "Angular Renderer2.listen" for. Global targets
324
+ // (window/document/body) have no Fabric node, so they no-op.
325
+ listen(
326
+ target: unknown,
327
+ eventName: string,
328
+ callback: (event: unknown) => boolean | void,
329
+ ): () => void {
330
+ if (!isSymbioteNode(target)) return () => {};
331
+ setEventListener(target, eventName, callback);
332
+ return () => setEventListener(target, eventName, undefined);
333
+ }
334
+ }
335
+
336
+ // Provided to Angular as RendererFactory2; createRenderer returns the single
337
+ // surface-bound renderer for every component (begin/end commit-coalescing is unnecessary —
338
+ // requestCommit already microtask-coalesces).
339
+ export class SymbioteRendererFactory implements RendererFactory2 {
340
+ private renderer: SymbioteRenderer | undefined;
341
+
342
+ constructor(private readonly surface: SymbioteSurface) {}
343
+
344
+ createRenderer(_hostElement: unknown, _type: RendererType2 | null): Renderer2 {
345
+ return (this.renderer ??= new SymbioteRenderer(this.surface));
346
+ }
347
+ }