@symbiote-native/angular 0.6.0 → 0.6.2

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 (193) hide show
  1. package/README.md +1 -1
  2. package/babel-register-composed.cjs +11 -3
  3. package/build/angular/anchor-host-registry.d.ts +2 -0
  4. package/build/angular/anchor-host-registry.js +84 -0
  5. package/build/angular/anchor-host-registry.js.map +1 -0
  6. package/build/angular/components/input-accessory-view/index.d.ts +2 -1
  7. package/build/angular/components/input-accessory-view/index.js +28 -39
  8. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  9. package/build/angular/components/scroll-view/projection.js +70 -48
  10. package/build/angular/components/scroll-view/projection.js.map +1 -1
  11. package/build/angular/components/scroll-view/shared.js +28 -25
  12. package/build/angular/components/scroll-view/shared.js.map +1 -1
  13. package/build/angular/components/scroll-view/sticky-header.d.ts +6 -7
  14. package/build/angular/components/scroll-view/sticky-header.js +82 -65
  15. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  16. package/build/angular/components/touchable/index.d.ts +2 -5
  17. package/build/angular/components/touchable/index.js +36 -56
  18. package/build/angular/components/touchable/index.js.map +1 -1
  19. package/build/angular/components/virtualized-list/index.d.ts +5 -20
  20. package/build/angular/components/virtualized-list/index.js +145 -275
  21. package/build/angular/components/virtualized-list/index.js.map +1 -1
  22. package/build/angular/components/virtualized-section-list/index.js +2 -4
  23. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  24. package/build/angular/index.d.ts +2 -1
  25. package/build/angular/index.js +6 -1
  26. package/build/angular/index.js.map +1 -1
  27. package/build/angular/renderer/index.d.ts +0 -1
  28. package/build/angular/renderer/index.js +5 -60
  29. package/build/angular/renderer/index.js.map +1 -1
  30. package/package.json +8 -4
  31. package/src/anchor-host-registry.ts +88 -0
  32. package/src/components/input-accessory-view/index.ts +30 -39
  33. package/src/components/scroll-view/projection.ts +76 -46
  34. package/src/components/scroll-view/shared.ts +27 -19
  35. package/src/components/scroll-view/sticky-header.ts +89 -64
  36. package/src/components/touchable/index.ts +52 -66
  37. package/src/components/virtualized-list/index.ts +157 -368
  38. package/src/components/virtualized-section-list/index.ts +6 -3
  39. package/src/index.ts +6 -1
  40. package/src/renderer/index.ts +5 -63
  41. package/build/bootstrap.d.ts +0 -7
  42. package/build/bootstrap.js +0 -14
  43. package/build/components/activity-indicator/index.android.d.ts +0 -9
  44. package/build/components/activity-indicator/index.android.js +0 -64
  45. package/build/components/activity-indicator/index.d.ts +0 -1
  46. package/build/components/activity-indicator/index.ios.d.ts +0 -5
  47. package/build/components/activity-indicator/index.ios.js +0 -64
  48. package/build/components/activity-indicator/index.js +0 -3
  49. package/build/components/activity-indicator/shared.d.ts +0 -64
  50. package/build/components/activity-indicator/shared.js +0 -424
  51. package/build/components/button.d.ts +0 -60
  52. package/build/components/button.js +0 -456
  53. package/build/components/flat-list/index.d.ts +0 -119
  54. package/build/components/flat-list/index.js +0 -691
  55. package/build/components/image/index.android.d.ts +0 -7
  56. package/build/components/image/index.android.js +0 -88
  57. package/build/components/image/index.d.ts +0 -1
  58. package/build/components/image/index.ios.d.ts +0 -7
  59. package/build/components/image/index.ios.js +0 -88
  60. package/build/components/image/index.js +0 -2
  61. package/build/components/image/shared.d.ts +0 -101
  62. package/build/components/image/shared.js +0 -405
  63. package/build/components/image-background.d.ts +0 -78
  64. package/build/components/image-background.js +0 -583
  65. package/build/components/input-accessory-view/index.d.ts +0 -56
  66. package/build/components/input-accessory-view/index.js +0 -403
  67. package/build/components/keyboard-avoiding-view/index.d.ts +0 -79
  68. package/build/components/keyboard-avoiding-view/index.js +0 -504
  69. package/build/components/modal/index.d.ts +0 -89
  70. package/build/components/modal/index.js +0 -529
  71. package/build/components/pressable/index.d.ts +0 -120
  72. package/build/components/pressable/index.js +0 -639
  73. package/build/components/refresh-control/index.d.ts +0 -83
  74. package/build/components/refresh-control/index.js +0 -509
  75. package/build/components/safe-area-view/index.d.ts +0 -54
  76. package/build/components/safe-area-view/index.js +0 -388
  77. package/build/components/scroll-view/index.android.d.ts +0 -16
  78. package/build/components/scroll-view/index.android.js +0 -228
  79. package/build/components/scroll-view/index.d.ts +0 -3
  80. package/build/components/scroll-view/index.ios.d.ts +0 -6
  81. package/build/components/scroll-view/index.ios.js +0 -113
  82. package/build/components/scroll-view/index.js +0 -5
  83. package/build/components/scroll-view/projection.d.ts +0 -32
  84. package/build/components/scroll-view/projection.js +0 -311
  85. package/build/components/scroll-view/shared.d.ts +0 -225
  86. package/build/components/scroll-view/shared.js +0 -757
  87. package/build/components/scroll-view/sticky-header.d.ts +0 -38
  88. package/build/components/scroll-view/sticky-header.js +0 -257
  89. package/build/components/section-list/index.d.ts +0 -113
  90. package/build/components/section-list/index.js +0 -768
  91. package/build/components/switch/index.android.d.ts +0 -17
  92. package/build/components/switch/index.android.js +0 -107
  93. package/build/components/switch/index.d.ts +0 -1
  94. package/build/components/switch/index.ios.d.ts +0 -16
  95. package/build/components/switch/index.ios.js +0 -106
  96. package/build/components/switch/index.js +0 -3
  97. package/build/components/switch/shared.d.ts +0 -37
  98. package/build/components/switch/shared.js +0 -176
  99. package/build/components/text-input.d.ts +0 -150
  100. package/build/components/text-input.js +0 -821
  101. package/build/components/touchable/index.d.ts +0 -241
  102. package/build/components/touchable/index.js +0 -1672
  103. package/build/components/touchable-native-feedback/index.d.ts +0 -80
  104. package/build/components/touchable-native-feedback/index.js +0 -545
  105. package/build/components/virtualized-list/directives.d.ts +0 -41
  106. package/build/components/virtualized-list/directives.js +0 -249
  107. package/build/components/virtualized-list/index.d.ts +0 -265
  108. package/build/components/virtualized-list/index.js +0 -1343
  109. package/build/components/virtualized-section-list/directives.d.ts +0 -28
  110. package/build/components/virtualized-section-list/directives.js +0 -148
  111. package/build/components/virtualized-section-list/index.d.ts +0 -175
  112. package/build/components/virtualized-section-list/index.js +0 -854
  113. package/build/components.d.ts +0 -39
  114. package/build/components.js +0 -37
  115. package/build/create-portal/index.d.ts +0 -17
  116. package/build/create-portal/index.js +0 -128
  117. package/build/create-tunnel/index.d.ts +0 -29
  118. package/build/create-tunnel/index.js +0 -181
  119. package/build/descriptor-to-angular/index.d.ts +0 -19
  120. package/build/descriptor-to-angular/index.js +0 -186
  121. package/build/host-instance/index.d.ts +0 -2
  122. package/build/host-instance/index.js +0 -36
  123. package/build/index.d.ts +0 -21
  124. package/build/index.js +0 -40
  125. package/build/modules/animated/animated-leaf-binder/index.d.ts +0 -14
  126. package/build/modules/animated/animated-leaf-binder/index.js +0 -90
  127. package/build/modules/animated/animated-leaf-binder.d.ts +0 -14
  128. package/build/modules/animated/animated-leaf-binder.js +0 -90
  129. package/build/modules/animated/create-animated-component.d.ts +0 -48
  130. package/build/modules/animated/create-animated-component.js +0 -425
  131. package/build/modules/animated/index.d.ts +0 -51
  132. package/build/modules/animated/index.js +0 -58
  133. package/build/modules/app-registry/index.d.ts +0 -7
  134. package/build/modules/app-registry/index.js +0 -22
  135. package/build/modules/status-bar/index.d.ts +0 -17
  136. package/build/modules/status-bar/index.js +0 -121
  137. package/build/primitives/index.d.ts +0 -32
  138. package/build/primitives/index.js +0 -386
  139. package/build/primitives/shared.d.ts +0 -83
  140. package/build/primitives/shared.js +0 -230
  141. package/build/render/index.d.ts +0 -11
  142. package/build/render/index.js +0 -170
  143. package/build/renderer/index.d.ts +0 -38
  144. package/build/renderer/index.js +0 -355
  145. package/build/services/color-scheme.service.d.ts +0 -13
  146. package/build/services/color-scheme.service.js +0 -72
  147. package/build/services/index.d.ts +0 -2
  148. package/build/services/index.js +0 -2
  149. package/build/services/window-dimensions.service.d.ts +0 -13
  150. package/build/services/window-dimensions.service.js +0 -83
  151. package/src/__tests__/responder-change-detection.test.ts +0 -102
  152. package/src/__tests__/responder-nested-cd.test.ts +0 -113
  153. package/src/angular-gaps.test.ts +0 -142
  154. package/src/angular-imports.test.ts +0 -100
  155. package/src/components/activity-indicator/activity-indicator-class-style.test.ts +0 -44
  156. package/src/components/activity-indicator/activity-indicator.test.ts +0 -131
  157. package/src/components/flat-list/column-wrapper-style-class.test.ts +0 -116
  158. package/src/components/flat-list/flat-list-array-style.test.ts +0 -131
  159. package/src/components/flat-list/flat-list-class-style.test.ts +0 -74
  160. package/src/components/flat-list/flat-list-scroll-containment.test.ts +0 -107
  161. package/src/components/flat-list/flat-list.test.ts +0 -99
  162. package/src/components/image/image-class-style.test.ts +0 -46
  163. package/src/components/image-background-class-style.test.ts +0 -48
  164. package/src/components/image-background-style-class.test.ts +0 -79
  165. package/src/components/input-accessory-view/input-accessory-view.test.ts +0 -43
  166. package/src/components/keyboard-avoiding-view/keyboard-avoiding-view.test.ts +0 -54
  167. package/src/components/modal/modal.test.ts +0 -43
  168. package/src/components/pressable/pressable.test.ts +0 -85
  169. package/src/components/refresh-control/refresh-control.test.ts +0 -41
  170. package/src/components/safe-area-view/safe-area-view.test.ts +0 -43
  171. package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +0 -123
  172. package/src/components/scroll-view/content-container-style-class.test.ts +0 -75
  173. package/src/components/scroll-view/scroll-view-class-style.test.ts +0 -134
  174. package/src/components/scroll-view/scroll-view-projection.test.ts +0 -194
  175. package/src/components/switch/android-switch-class.test.ts +0 -42
  176. package/src/components/switch/switch.test.ts +0 -92
  177. package/src/components/text-input-class-style.test.ts +0 -54
  178. package/src/components/touchable/touchable.test.ts +0 -102
  179. package/src/components/touchable-native-feedback/touchable-native-feedback.test.ts +0 -49
  180. package/src/components/virtualized-list/virtualized-list-class-style.test.ts +0 -76
  181. package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +0 -60
  182. package/src/create-portal/create-portal.test.ts +0 -90
  183. package/src/create-tunnel/create-tunnel.test.ts +0 -120
  184. package/src/descriptor-to-angular/descriptor-outlet.test.ts +0 -94
  185. package/src/host-instance/host-instance.test.ts +0 -49
  186. package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +0 -116
  187. package/src/modules/animated/animated-leaf-binder.test.ts +0 -116
  188. package/src/modules/animated/animated-scroll-view.test.ts +0 -102
  189. package/src/modules/app-registry/app-registry.test.ts +0 -106
  190. package/src/modules/status-bar/status-bar.test.ts +0 -50
  191. package/src/render/render.test.ts +0 -147
  192. package/src/renderer/renderer.test.ts +0 -202
  193. package/src/services/services.test.ts +0 -163
@@ -1,355 +0,0 @@
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
- import { appendChild, createAnchor, createElement, createRawText, dlog, getExplicitStyle, insertBefore, isDebug, isSymbioteNode, removeChild, routeProp, setEventListener, setText, toPublicInstance, RAW_TEXT_COMPONENT, SymbioteSurface, } from '@symbiote-native/engine';
8
- import { descriptorFor } from '@symbiote-native/components';
9
- import { getScrollViewProjection, removeScrollViewProjectedChild, } from '../components/scroll-view/projection';
10
- function isSurface(parent) {
11
- return parent instanceof SymbioteSurface;
12
- }
13
- function isRawText(node) {
14
- return node.component === RAW_TEXT_COMPONENT;
15
- }
16
- function isRecord(value) {
17
- return typeof value === 'object' && value !== null;
18
- }
19
- // Diagnostic-only: tags each anchor with a sequential id so log lines can tell distinct
20
- // anchors apart (they all otherwise print the same generic '#anchor' component name).
21
- // Gated behind isDebug() so a production build pays zero cost (no WeakMap writes).
22
- let anchorDebugCounter = 0;
23
- const anchorDebugIds = new WeakMap();
24
- function tagAnchorForDebug(node) {
25
- if (isDebug()) {
26
- anchorDebugCounter += 1;
27
- anchorDebugIds.set(node, anchorDebugCounter);
28
- }
29
- return node;
30
- }
31
- // Diagnostic identity string for a renderer node/surface — component name, or 'surface'.
32
- // Null-tolerant: a diagnostic must never throw (Angular hands insertBefore a null refChild).
33
- function describeHost(node) {
34
- if (node === null || node === undefined)
35
- return 'null';
36
- if (isSurface(node))
37
- return 'surface';
38
- const anchorId = anchorDebugIds.get(node);
39
- return anchorId !== undefined ? `${node.component}#${anchorId}` : node.component;
40
- }
41
- const PRIMITIVE_SELECTOR_ALIAS = {
42
- // Public ergonomic selectors map directly to the engine primitive descriptors.
43
- View: 'symbiote-view',
44
- Text: 'symbiote-text',
45
- };
46
- // A composed component's selector must be listed so createElement gives it a non-painting
47
- // anchor host (see the set comment below). Tests register their own child-component selectors
48
- // through this at module load; app-authored composed components can too.
49
- export function registerComposedComponent(selector) {
50
- ANCHOR_HOST_COMPONENTS.add(selector.toLowerCase());
51
- }
52
- // Lowercased at construction — createElement's lookup and registerComposedComponent's insert
53
- // both normalize to lowercase (Angular lowercases a dynamically-mounted component's selector at
54
- // runtime, see §11a), so the literal entries below must match that or every capitalized one
55
- // (all but the handful already written lowercase) silently never matches .has() and falls
56
- // through to a real Fabric createNode — the exact "Unimplemented component" / extra-wrapper-node
57
- // bug this Set exists to prevent.
58
- const ANCHOR_HOST_COMPONENTS = new Set([
59
- // Composed Angular components render their real Fabric descriptor tree from the template;
60
- // their Angular host element is only a framework bookkeeping node and must not paint. This
61
- // is NOT limited to adapter-authored components — ANY custom composed @Component used as a
62
- // plain <Tag> inside another template needs its selector listed here too, or Angular's
63
- // automatic host-element creation for it falls through to a raw Fabric createNode call with
64
- // an unrecognized view name, which paints RN's own "Unimplemented component: <Tag>" fallback
65
- // view instead (a real device-visible bug, not a silent no-op). This Set holds only
66
- // adapter/engine-owned selectors; app code and third-party packages self-register their own
67
- // composed components through registerComposedComponent instead of being hardcoded here.
68
- 'ActivityIndicator',
69
- 'Button',
70
- 'FlatList',
71
- 'AnimatedView',
72
- 'symbiote-animated-view',
73
- 'AnimatedText',
74
- 'symbiote-animated-text',
75
- 'AnimatedImage',
76
- 'symbiote-animated-image',
77
- 'AnimatedScrollView',
78
- 'symbiote-animated-scroll-view',
79
- 'symbiote-descriptor-outlet',
80
- 'tunnel-out',
81
- 'Image',
82
- 'ImageBackground',
83
- 'InputAccessoryView',
84
- 'KeyboardAvoidingView',
85
- 'Modal',
86
- 'Pressable',
87
- 'RefreshControl',
88
- 'SafeAreaView',
89
- 'ScrollView',
90
- 'ScrollViewStickyHeader',
91
- 'SectionList',
92
- 'symbiote-sticky-header',
93
- 'StatusBar',
94
- 'Switch',
95
- 'Text',
96
- 'TextInput',
97
- 'TouchableHighlight',
98
- 'TouchableNativeFeedback',
99
- 'TouchableOpacity',
100
- 'TouchableWithoutFeedback',
101
- 'VirtualizedList',
102
- 'VirtualizedSectionList',
103
- 'symbiote-pressable',
104
- ].map(selector => selector.toLowerCase()));
105
- // Inserting a bare raw-text node anywhere but inside a <Text> is invalid in Fabric (a
106
- // stray RCTRawText would paint). Angular's ɵɵtext only ever lands text inside a <Text>,
107
- // but guard anyway for parity with the Vue adapter and to fail loudly on a bad template.
108
- function assertTextPlacement(child, parent) {
109
- if (isRawText(child) && (isSurface(parent) || !parent.isText)) {
110
- throw new Error(`Text string "${String(child.props.text)}" must be rendered inside a <Text>`);
111
- }
112
- }
113
- // One renderer per mounted surface. Every mutation asks the surface to (microtask-
114
- // coalesced) recommit — the same seam Vue uses; a burst of Angular change-detection
115
- // mutations collapses into one completeRoot.
116
- export class SymbioteRenderer {
117
- surface;
118
- data = {};
119
- // Angular calls destroyNode per-node only when this is non-null; teardown happens in
120
- // render.ts (unmount), so per-node cleanup is a no-op.
121
- destroyNode = null;
122
- constructor(surface) {
123
- this.surface = surface;
124
- }
125
- destroy() { }
126
- createElement(name) {
127
- // `name` is the component's host tag — a symbiote intrinsic (`symbiote-view`,
128
- // `symbiote-text`, …), a public ergonomic alias (`View`, `Text`), or a raw Fabric view
129
- // name for a native leaf. Public aliases are normalized to their engine primitive name
130
- // before descriptor lookup. descriptorFor resolves it; an unknown `symbiote-*` is a typo,
131
- // any other string flows through as a raw Fabric name (events/processors derived from its
132
- // ViewConfig). toPublicInstance grafts the imperative API (measure / setNativeProps /
133
- // focus) onto the raw node in place, returning the SAME identity the commit mirror keys on.
134
- const engineName = PRIMITIVE_SELECTOR_ALIAS[name] ?? name;
135
- if (ANCHOR_HOST_COMPONENTS.has(engineName.toLowerCase())) {
136
- const anchor = tagAnchorForDebug(createAnchor());
137
- dlog(`angular createElement ${name} -> anchor host ${describeHost(anchor)}`);
138
- return anchor;
139
- }
140
- const descriptor = descriptorFor(engineName);
141
- const node = createElement(descriptor.component, descriptor.isText);
142
- dlog(`angular createElement ${name} -> ${descriptor.component}`);
143
- return toPublicInstance(node);
144
- }
145
- createComment() {
146
- // Angular structural directives (*ngIf / @if / @for) need anchor nodes to track
147
- // position. A real retained node the commit walk SKIPS — no native view. Twin of the
148
- // Vue createComment path.
149
- const anchor = tagAnchorForDebug(createAnchor());
150
- dlog(`Angular renderer createComment -> ${describeHost(anchor)}`);
151
- return anchor;
152
- }
153
- createText(value) {
154
- return createRawText(value);
155
- }
156
- appendChild(parent, newChild) {
157
- // Angular defers insertion for content awaiting its host component's own projection
158
- // (see parentNode below) — mirrors Angular's own `if (parentRNode !== null)` guard in
159
- // addLViewToLContainer: skip silently now, the later projection pass places it correctly.
160
- if (parent === null)
161
- return;
162
- assertTextPlacement(newChild, parent);
163
- if (isSurface(parent)) {
164
- dlog(`Angular renderer appendChild parent=surface child=${describeHost(newChild)}`);
165
- parent.appendChild(newChild);
166
- }
167
- else {
168
- const projection = getScrollViewProjection(parent);
169
- dlog(`Angular renderer appendChild parent=${describeHost(parent)} child=${describeHost(newChild)} projection=${projection !== undefined}`);
170
- if (projection !== undefined) {
171
- projection.appendProjectedChild(parent, newChild, (target, child) => appendChild(target, child));
172
- }
173
- else {
174
- appendChild(parent, newChild);
175
- }
176
- }
177
- this.surface.requestCommit();
178
- }
179
- insertBefore(parent, newChild, refChild) {
180
- if (parent === null)
181
- return; // see the appendChild guard above
182
- assertTextPlacement(newChild, parent);
183
- if (isSurface(parent)) {
184
- dlog(`Angular renderer insertBefore parent=surface child=${describeHost(newChild)} ref=${refChild ? describeHost(refChild) : 'null'}`);
185
- if (refChild)
186
- parent.insertBefore(newChild, refChild);
187
- else
188
- parent.appendChild(newChild);
189
- }
190
- else {
191
- const projection = getScrollViewProjection(parent);
192
- dlog(`Angular renderer insertBefore parent=${describeHost(parent)} child=${describeHost(newChild)} ref=${refChild ? describeHost(refChild) : 'null'} projection=${projection !== undefined}`);
193
- if (projection !== undefined) {
194
- projection.insertProjectedChild(parent, newChild, refChild, (target, child, before) => {
195
- if (before === undefined)
196
- appendChild(target, child);
197
- else
198
- insertBefore(target, child, before);
199
- });
200
- }
201
- else if (refChild) {
202
- insertBefore(parent, newChild, refChild);
203
- }
204
- else {
205
- appendChild(parent, newChild);
206
- }
207
- }
208
- this.surface.requestCommit();
209
- }
210
- removeChild(_parent, oldChild) {
211
- // Detach from the child's own retained parent (a top-level node lives in
212
- // surface.children with no parent). Angular's `parent` arg is ignored in favor of the
213
- // authoritative link, mirroring the Vue adapter's remove.
214
- const angularParent = _parent !== null ? describeHost(_parent) : 'null';
215
- const retainedParent = oldChild.parent !== undefined ? describeHost(oldChild.parent) : 'none';
216
- const wasProjected = removeScrollViewProjectedChild(oldChild, (parent, child) => removeChild(parent, child));
217
- dlog(`Angular renderer removeChild angularParent=${angularParent} retainedParent=${retainedParent} child=${describeHost(oldChild)} viaProjection=${wasProjected}`);
218
- if (!wasProjected) {
219
- const parent = oldChild.parent;
220
- if (parent !== undefined)
221
- removeChild(parent, oldChild);
222
- else
223
- this.surface.removeChild(oldChild);
224
- }
225
- this.surface.requestCommit();
226
- }
227
- // FlatList/VirtualizedList cells are content projected into a component host (our
228
- // ANCHOR_HOST_COMPONENTS, e.g. ScrollView) — Angular's own addLViewToLContainer
229
- // (.vendors/angular node_manipulation.ts/container.ts) treats a null parent here as "defer —
230
- // the child component's own <ng-content>/ɵɵprojection will place this once its structure
231
- // resolves" (e.g. ScrollView's `@if(isHorizontal)` branch). Renderer2's contract types this
232
- // return as nullable for exactly that reason; returning `this.surface` as a non-null fallback
233
- // defeated that defer check and caused premature top-level insertion (2026-07: FlatList cells
234
- // rendered outside their ScrollView).
235
- //
236
- // Safe only because appendChild/insertBefore above now also treat a null parent as "skip, wait
237
- // for projection" — a second Angular call site (`insertAnchorNode`, hit whenever a directive
238
- // does `inject(ViewContainerRef)`, e.g. VListOutletDirective) forwards this null straight into
239
- // insertBefore without checking it; without that guard it crashed on-device.
240
- parentNode(node) {
241
- return node.parent ?? null;
242
- }
243
- nextSibling(node) {
244
- const siblings = node.parent !== undefined ? node.parent.children : this.surface.children;
245
- const index = siblings.indexOf(node);
246
- return index >= 0 ? (siblings[index + 1] ?? null) : null;
247
- }
248
- // locateHostElement always routes createComponent's `hostElement` THROUGH here as
249
- // `selectorOrNode` (Angular's own core.mjs) — it is never bypassed just because a real
250
- // object (vs. a selector string) was given. A string only reaches us on the (unused here)
251
- // selector-string bootstrap path, so the surface is the fallback for that case only.
252
- selectRootElement(selectorOrNode) {
253
- return typeof selectorOrNode === 'string' ? this.surface : selectorOrNode;
254
- }
255
- setAttribute(el, name, value) {
256
- if (isSurface(el))
257
- return;
258
- routeProp(el, name, value);
259
- this.surface.requestCommit();
260
- }
261
- removeAttribute(el, name) {
262
- if (isSurface(el))
263
- return;
264
- routeProp(el, name, undefined);
265
- this.surface.requestCommit();
266
- }
267
- // Ivy compiles every class= / [class.foo] / [ngClass] form down to per-token addClass/
268
- // removeClass calls (never a single setAttribute('class', ...) call), so a per-node token set
269
- // is accumulated here and re-joined into one string on every change, then handed to
270
- // routeProp('class', ...) exactly like Vue's template `class="..."` and React's JSX
271
- // `className="..."` — all three resolve through the SAME centralized class+style merge in
272
- // core/engine/src/node.ts, so a class registered via the SFC/CSS-Modules style compiler
273
- // resolves identically regardless of adapter.
274
- classTokens = new WeakMap();
275
- addClass(el, name) {
276
- if (isSurface(el))
277
- return;
278
- const tokens = this.classTokens.get(el) ?? new Set();
279
- tokens.add(name);
280
- this.classTokens.set(el, tokens);
281
- routeProp(el, 'class', [...tokens].join(' '));
282
- this.surface.requestCommit();
283
- }
284
- removeClass(el, name) {
285
- if (isSurface(el))
286
- return;
287
- const tokens = this.classTokens.get(el);
288
- if (tokens === undefined)
289
- return;
290
- tokens.delete(name);
291
- routeProp(el, 'class', tokens.size > 0 ? [...tokens].join(' ') : undefined);
292
- this.surface.requestCommit();
293
- }
294
- // Angular decomposes a [style] binding into per-key setStyle calls (ɵɵstyleMap). RN wants
295
- // the whole style object as one `style` prop, so merge each key into it — onto the explicit
296
- // style half tracked by routeProp's centralized class+style merge (core/engine/src/node.ts),
297
- // NOT el.props.style directly: that may now be the [classStyle, explicitStyle] array the
298
- // merge writes, and spreading an array as a record would silently produce numeric-index keys.
299
- setStyle(el, style, value) {
300
- if (isSurface(el))
301
- return;
302
- const current = getExplicitStyle(el);
303
- const base = isRecord(current) ? current : {};
304
- routeProp(el, 'style', { ...base, [style]: value });
305
- this.surface.requestCommit();
306
- }
307
- removeStyle(el, style) {
308
- if (isSurface(el))
309
- return;
310
- const current = getExplicitStyle(el);
311
- if (!isRecord(current))
312
- return;
313
- const { [style]: _removed, ...rest } = current;
314
- routeProp(el, 'style', rest);
315
- this.surface.requestCommit();
316
- }
317
- // [prop]="x" bindings. routeProp makes the prop-vs-event decision from the node's
318
- // ViewConfig (identical to React/Vue), so the whole flat-bag prop layer is shared.
319
- setProperty(el, name, value) {
320
- if (isSurface(el))
321
- return;
322
- routeProp(el, name, value);
323
- this.surface.requestCommit();
324
- }
325
- setValue(node, value) {
326
- // A useful permanent seam: text mutations are low-frequency and the one place a stale
327
- // `{{binding}}` (a change-detection gap) shows up as "the setValue never fired".
328
- dlog(`Angular renderer setValue "${value}" on ${describeHost(node)}`);
329
- setText(node, value);
330
- this.surface.requestCommit();
331
- }
332
- // (event)="x" bindings. Angular hands the event name EXPLICITLY (no onX->x inference),
333
- // so we drive the engine's structural event channel directly — the path setEventListener
334
- // in core/engine/src/node.ts already names "Angular Renderer2.listen" for. Global targets
335
- // (window/document/body) have no Fabric node, so they no-op.
336
- listen(target, eventName, callback) {
337
- if (!isSymbioteNode(target))
338
- return () => { };
339
- setEventListener(target, eventName, callback);
340
- return () => setEventListener(target, eventName, undefined);
341
- }
342
- }
343
- // Provided to Angular as RendererFactory2; createRenderer returns the single
344
- // surface-bound renderer for every component (begin/end commit-coalescing is unnecessary —
345
- // requestCommit already microtask-coalesces).
346
- export class SymbioteRendererFactory {
347
- surface;
348
- renderer;
349
- constructor(surface) {
350
- this.surface = surface;
351
- }
352
- createRenderer(_hostElement, _type) {
353
- return (this.renderer ??= new SymbioteRenderer(this.surface));
354
- }
355
- }
@@ -1,13 +0,0 @@
1
- import { type IColorSchemeName } from '@symbiote-native/engine';
2
- /**
3
- * Angular twin of React's `useColorScheme` and Vue's `useColorScheme` composable. The service
4
- * holds a Signal that tracks the device color scheme; consumers read it via `inject`:
5
- *
6
- * readonly colorScheme = inject(ColorSchemeService).colorScheme;
7
- *
8
- * State and lifecycle live in DI, not in a function closure.
9
- */
10
- export declare class ColorSchemeService {
11
- readonly colorScheme: import("@angular/core").WritableSignal<IColorSchemeName | null>;
12
- constructor();
13
- }
@@ -1,72 +0,0 @@
1
- var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
- function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
- var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
- var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
- var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
- var _, done = false;
7
- for (var i = decorators.length - 1; i >= 0; i--) {
8
- var context = {};
9
- for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
- for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
- context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
- var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
- if (kind === "accessor") {
14
- if (result === void 0) continue;
15
- if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
- if (_ = accept(result.get)) descriptor.get = _;
17
- if (_ = accept(result.set)) descriptor.set = _;
18
- if (_ = accept(result.init)) initializers.unshift(_);
19
- }
20
- else if (_ = accept(result)) {
21
- if (kind === "field") initializers.unshift(_);
22
- else descriptor[key] = _;
23
- }
24
- }
25
- if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
- done = true;
27
- };
28
- var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
- var useValue = arguments.length > 2;
30
- for (var i = 0; i < initializers.length; i++) {
31
- value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
- }
33
- return useValue ? value : void 0;
34
- };
35
- import { DestroyRef, inject, Injectable, signal } from '@angular/core';
36
- import { Appearance } from '@symbiote-native/engine';
37
- /**
38
- * Angular twin of React's `useColorScheme` and Vue's `useColorScheme` composable. The service
39
- * holds a Signal that tracks the device color scheme; consumers read it via `inject`:
40
- *
41
- * readonly colorScheme = inject(ColorSchemeService).colorScheme;
42
- *
43
- * State and lifecycle live in DI, not in a function closure.
44
- */
45
- let ColorSchemeService = (() => {
46
- let _classDecorators = [Injectable({ providedIn: 'root' })];
47
- let _classDescriptor;
48
- let _classExtraInitializers = [];
49
- let _classThis;
50
- var ColorSchemeService = class {
51
- static { _classThis = this; }
52
- static {
53
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
54
- __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
55
- ColorSchemeService = _classThis = _classDescriptor.value;
56
- if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
57
- __runInitializers(_classThis, _classExtraInitializers);
58
- }
59
- colorScheme = signal(Appearance.getColorScheme());
60
- constructor() {
61
- const destroyRef = inject(DestroyRef);
62
- const subscription = Appearance.addChangeListener(preferences => {
63
- this.colorScheme.set(preferences.colorScheme);
64
- });
65
- destroyRef.onDestroy(() => {
66
- subscription.remove();
67
- });
68
- }
69
- };
70
- return ColorSchemeService = _classThis;
71
- })();
72
- export { ColorSchemeService };
@@ -1,2 +0,0 @@
1
- export { ColorSchemeService } from './color-scheme.service';
2
- export { WindowDimensionsService } from './window-dimensions.service';
@@ -1,2 +0,0 @@
1
- export { ColorSchemeService } from './color-scheme.service';
2
- export { WindowDimensionsService } from './window-dimensions.service';
@@ -1,13 +0,0 @@
1
- import { type IDisplayMetrics } from '@symbiote-native/engine';
2
- /**
3
- * Angular twin of React's `useWindowDimensions` and Vue's `useWindowDimensions` composable. The
4
- * service holds a Signal that tracks window metrics; consumers read it via `inject`:
5
- *
6
- * readonly dimensions = inject(WindowDimensionsService).dimensions;
7
- *
8
- * State and lifecycle live in DI, not in a function closure.
9
- */
10
- export declare class WindowDimensionsService {
11
- readonly dimensions: import("@angular/core").WritableSignal<IDisplayMetrics>;
12
- constructor();
13
- }
@@ -1,83 +0,0 @@
1
- var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
- function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
- var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
- var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
- var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
- var _, done = false;
7
- for (var i = decorators.length - 1; i >= 0; i--) {
8
- var context = {};
9
- for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
- for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
- context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
- var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
- if (kind === "accessor") {
14
- if (result === void 0) continue;
15
- if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
- if (_ = accept(result.get)) descriptor.get = _;
17
- if (_ = accept(result.set)) descriptor.set = _;
18
- if (_ = accept(result.init)) initializers.unshift(_);
19
- }
20
- else if (_ = accept(result)) {
21
- if (kind === "field") initializers.unshift(_);
22
- else descriptor[key] = _;
23
- }
24
- }
25
- if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
- done = true;
27
- };
28
- var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
- var useValue = arguments.length > 2;
30
- for (var i = 0; i < initializers.length; i++) {
31
- value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
- }
33
- return useValue ? value : void 0;
34
- };
35
- import { DestroyRef, inject, Injectable, signal } from '@angular/core';
36
- import { Dimensions } from '@symbiote-native/engine';
37
- /**
38
- * Angular twin of React's `useWindowDimensions` and Vue's `useWindowDimensions` composable. The
39
- * service holds a Signal that tracks window metrics; consumers read it via `inject`:
40
- *
41
- * readonly dimensions = inject(WindowDimensionsService).dimensions;
42
- *
43
- * State and lifecycle live in DI, not in a function closure.
44
- */
45
- let WindowDimensionsService = (() => {
46
- let _classDecorators = [Injectable({ providedIn: 'root' })];
47
- let _classDescriptor;
48
- let _classExtraInitializers = [];
49
- let _classThis;
50
- var WindowDimensionsService = class {
51
- static { _classThis = this; }
52
- static {
53
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
54
- __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
55
- WindowDimensionsService = _classThis = _classDescriptor.value;
56
- if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
57
- __runInitializers(_classThis, _classExtraInitializers);
58
- }
59
- dimensions = signal(Dimensions.get('window'));
60
- constructor() {
61
- const destroyRef = inject(DestroyRef);
62
- const handleChange = (window) => {
63
- const current = this.dimensions();
64
- if (current.width !== window.width ||
65
- current.height !== window.height ||
66
- current.scale !== window.scale ||
67
- current.fontScale !== window.fontScale) {
68
- this.dimensions.set(window);
69
- }
70
- };
71
- const subscription = Dimensions.addEventListener('change', (set) => {
72
- handleChange(set.window);
73
- });
74
- // Re-check once after subscribing to close the gap between construction and the listener.
75
- handleChange(Dimensions.get('window'));
76
- destroyRef.onDestroy(() => {
77
- subscription.remove();
78
- });
79
- }
80
- };
81
- return WindowDimensionsService = _classThis;
82
- })();
83
- export { WindowDimensionsService };
@@ -1,102 +0,0 @@
1
- // A flat-bag `onX` responder callback is invoked by the engine's event dispatch, entirely
2
- // outside Angular. In Angular 20 components compile as SignalView (not CheckAlways), so a plain
3
- // state mutation inside such a callback dirties nothing and the template stays stale — the "pan
4
- // does nothing" bug, while React/Vue repainted the same demo. SymbioteHostPropsDirective now
5
- // calls `cdr.markForCheck()` after each such callback (flags the component + ancestors with
6
- // RefreshView and notifies the scheduler), so the mutation repaints. This is the flat-root case;
7
- // responder-nested-cd.test.ts covers the App→child nesting the real app has.
8
-
9
- import '@angular/compiler';
10
- import { Component } from '@angular/core';
11
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
12
- import { installFabric, type IFakeNode } from '@symbiote-native/test-utils';
13
-
14
- import { mount, unmount } from '../render';
15
- import { ViewHost as View, TextHost as Text, SymbioteHostPropsDirective } from '../primitives';
16
-
17
- const ROOT_TAG = 970;
18
- const TOUCH_START = 'topTouchStart';
19
- const TOUCH_MOVE = 'topTouchMove';
20
- const TOUCH_END = 'topTouchEnd';
21
-
22
- const fabric = installFabric();
23
-
24
- // Fabric is clone-on-write: a prop update yields a NEW node object in the committed tree,
25
- // never in `created`. Walk the live committed child-set for post-mutation assertions.
26
- function findCommitted(predicate: (node: IFakeNode) => boolean): IFakeNode | undefined {
27
- const stack = [...fabric.committed];
28
- while (stack.length > 0) {
29
- const node = stack.pop();
30
- if (node === undefined) continue;
31
- if (predicate(node)) return node;
32
- stack.push(...node.children);
33
- }
34
- return undefined;
35
- }
36
-
37
- function statusText(testID: string): string | undefined {
38
- const node = findCommitted(n => n.props.testID === testID);
39
- const raw = node?.children[0];
40
- return typeof raw?.props.text === 'string' ? raw.props.text : undefined;
41
- }
42
-
43
- // The stable SymbioteNode (event target). Its identity and listener map survive clone-on-
44
- // write, so firing on the first-created handle reaches the live listeners.
45
- function handleFor(testID: string): unknown {
46
- const node = fabric.find(n => n.props.testID === testID);
47
- if (!node) throw new Error(`no node created with testID=${testID}`);
48
- return node.instanceHandle;
49
- }
50
-
51
- const flush = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
52
-
53
- @Component({
54
- selector: 'symbiote-responder-cd-host',
55
- standalone: true,
56
- imports: [View, Text, SymbioteHostPropsDirective],
57
- template: `
58
- <View [symbioteHostProps]="handlers"></View>
59
- <Text [symbioteHostProps]="statusProps">{{ status }}</Text>
60
- `,
61
- })
62
- class ResponderCdHost {
63
- status = 'idle';
64
- statusProps = { testID: 'status' };
65
- handlers = {
66
- testID: 'chip',
67
- onStartShouldSetResponder: () => true,
68
- onResponderGrant: () => {
69
- this.status = 'granted';
70
- },
71
- onResponderMove: () => {
72
- this.status = 'moving';
73
- },
74
- onResponderRelease: () => {
75
- this.status = 'released';
76
- },
77
- };
78
- }
79
-
80
- beforeEach(() => fabric.reset());
81
- afterEach(() => unmount(ROOT_TAG));
82
-
83
- describe('Angular responder callback triggers change detection', () => {
84
- it('re-renders bound state mutated inside a flat-bag responder callback', async () => {
85
- mount(ROOT_TAG, ResponderCdHost);
86
- await flush();
87
- expect(statusText('status')).toBe('idle');
88
-
89
- const chip = handleFor('chip');
90
- fabric.fireEvent(chip, TOUCH_START);
91
- await flush();
92
- expect(statusText('status')).toBe('granted');
93
-
94
- fabric.fireEvent(chip, TOUCH_MOVE);
95
- await flush();
96
- expect(statusText('status')).toBe('moving');
97
-
98
- fabric.fireEvent(chip, TOUCH_END);
99
- await flush();
100
- expect(statusText('status')).toBe('released');
101
- });
102
- });