@symbiote-native/angular 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +3 -3
  2. package/babel-lower-host-primitives.cjs +636 -0
  3. package/babel-register-composed.cjs +21 -3
  4. package/build/angular/anchor-host-registry.js +3 -1
  5. package/build/angular/anchor-host-registry.js.map +1 -1
  6. package/build/angular/bootstrap.js +1 -1
  7. package/build/angular/bootstrap.js.map +1 -1
  8. package/build/angular/components/activity-indicator/index.android.js +7 -2
  9. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  10. package/build/angular/components/activity-indicator/index.ios.js +3 -1
  11. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  12. package/build/angular/components/activity-indicator/shared.d.ts +1 -1
  13. package/build/angular/components/activity-indicator/shared.js +1 -1
  14. package/build/angular/components/activity-indicator/shared.js.map +1 -1
  15. package/build/angular/components/button.d.ts +3 -0
  16. package/build/angular/components/button.js +22 -5
  17. package/build/angular/components/button.js.map +1 -1
  18. package/build/angular/components/flat-list/index.d.ts +1 -1
  19. package/build/angular/components/flat-list/index.js +47 -14
  20. package/build/angular/components/flat-list/index.js.map +1 -1
  21. package/build/angular/components/image/index.android.js +4 -10
  22. package/build/angular/components/image/index.android.js.map +1 -1
  23. package/build/angular/components/image/index.ios.js +4 -10
  24. package/build/angular/components/image/index.ios.js.map +1 -1
  25. package/build/angular/components/image/shared.d.ts +4 -4
  26. package/build/angular/components/image/shared.js +51 -33
  27. package/build/angular/components/image/shared.js.map +1 -1
  28. package/build/angular/components/image-background.d.ts +3 -0
  29. package/build/angular/components/image-background.js +32 -9
  30. package/build/angular/components/image-background.js.map +1 -1
  31. package/build/angular/components/input-accessory-view/index.d.ts +2 -1
  32. package/build/angular/components/input-accessory-view/index.js +18 -16
  33. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  34. package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
  35. package/build/angular/components/keyboard-avoiding-view/index.js +56 -19
  36. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  37. package/build/angular/components/modal/index.d.ts +1 -0
  38. package/build/angular/components/modal/index.js +21 -10
  39. package/build/angular/components/modal/index.js.map +1 -1
  40. package/build/angular/components/pressable/index.d.ts +10 -5
  41. package/build/angular/components/pressable/index.js +61 -16
  42. package/build/angular/components/pressable/index.js.map +1 -1
  43. package/build/angular/components/refresh-control/index.d.ts +2 -1
  44. package/build/angular/components/refresh-control/index.js +13 -9
  45. package/build/angular/components/refresh-control/index.js.map +1 -1
  46. package/build/angular/components/safe-area-view/index.d.ts +3 -1
  47. package/build/angular/components/safe-area-view/index.js +22 -20
  48. package/build/angular/components/safe-area-view/index.js.map +1 -1
  49. package/build/angular/components/scroll-view/index.android.js +44 -14
  50. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  51. package/build/angular/components/scroll-view/index.ios.js +13 -5
  52. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  53. package/build/angular/components/scroll-view/projection.d.ts +32 -0
  54. package/build/angular/components/scroll-view/projection.js +212 -47
  55. package/build/angular/components/scroll-view/projection.js.map +1 -1
  56. package/build/angular/components/scroll-view/shared.d.ts +2 -0
  57. package/build/angular/components/scroll-view/shared.js +48 -11
  58. package/build/angular/components/scroll-view/shared.js.map +1 -1
  59. package/build/angular/components/scroll-view/sticky-header.js +7 -3
  60. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  61. package/build/angular/components/section-list/index.d.ts +9 -1
  62. package/build/angular/components/section-list/index.js +72 -18
  63. package/build/angular/components/section-list/index.js.map +1 -1
  64. package/build/angular/components/switch/index.android.js +23 -9
  65. package/build/angular/components/switch/index.android.js.map +1 -1
  66. package/build/angular/components/switch/index.ios.js +23 -9
  67. package/build/angular/components/switch/index.ios.js.map +1 -1
  68. package/build/angular/components/switch/shared.d.ts +1 -1
  69. package/build/angular/components/switch/shared.js +5 -2
  70. package/build/angular/components/switch/shared.js.map +1 -1
  71. package/build/angular/components/text-input.d.ts +8 -2
  72. package/build/angular/components/text-input.js +79 -34
  73. package/build/angular/components/text-input.js.map +1 -1
  74. package/build/angular/components/touchable/index.d.ts +36 -7
  75. package/build/angular/components/touchable/index.js +280 -72
  76. package/build/angular/components/touchable/index.js.map +1 -1
  77. package/build/angular/components/touchable-native-feedback/index.d.ts +2 -1
  78. package/build/angular/components/touchable-native-feedback/index.js +19 -17
  79. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  80. package/build/angular/components/virtualized-list/directives.js +4 -0
  81. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  82. package/build/angular/components/virtualized-list/index.d.ts +8 -2
  83. package/build/angular/components/virtualized-list/index.js +207 -61
  84. package/build/angular/components/virtualized-list/index.js.map +1 -1
  85. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  86. package/build/angular/components/virtualized-section-list/index.d.ts +21 -1
  87. package/build/angular/components/virtualized-section-list/index.js +100 -22
  88. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  89. package/build/angular/components.d.ts +2 -2
  90. package/build/angular/components.js +1 -1
  91. package/build/angular/components.js.map +1 -1
  92. package/build/angular/create-portal/index.d.ts +11 -2
  93. package/build/angular/create-portal/index.js +42 -12
  94. package/build/angular/create-portal/index.js.map +1 -1
  95. package/build/angular/create-tunnel/index.js.map +1 -1
  96. package/build/angular/descriptor-to-angular/index.js +3 -1
  97. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  98. package/build/angular/diagnostics.d.ts +33 -0
  99. package/build/angular/diagnostics.js +82 -0
  100. package/build/angular/diagnostics.js.map +1 -0
  101. package/build/angular/gate-demand.d.ts +37 -0
  102. package/build/angular/gate-demand.js +84 -0
  103. package/build/angular/gate-demand.js.map +1 -0
  104. package/build/angular/host-instance/index.js +1 -1
  105. package/build/angular/host-instance/index.js.map +1 -1
  106. package/build/angular/index.d.ts +6 -2
  107. package/build/angular/index.js +16 -1
  108. package/build/angular/index.js.map +1 -1
  109. package/build/angular/modules/animated/create-animated-component.d.ts +1 -0
  110. package/build/angular/modules/animated/create-animated-component.js +41 -18
  111. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  112. package/build/angular/modules/app-registry/index.js.map +1 -1
  113. package/build/angular/modules/status-bar/index.js +1 -1
  114. package/build/angular/modules/status-bar/index.js.map +1 -1
  115. package/build/angular/primitives/index.d.ts +31 -2
  116. package/build/angular/primitives/index.js +81 -3
  117. package/build/angular/primitives/index.js.map +1 -1
  118. package/build/angular/primitives/shared.d.ts +9 -1
  119. package/build/angular/primitives/shared.js +49 -3
  120. package/build/angular/primitives/shared.js.map +1 -1
  121. package/build/angular/register.d.ts +1 -0
  122. package/build/angular/register.js +35 -0
  123. package/build/angular/register.js.map +1 -0
  124. package/build/angular/render/index.js +51 -6
  125. package/build/angular/render/index.js.map +1 -1
  126. package/build/angular/renderer/index.d.ts +1 -0
  127. package/build/angular/renderer/index.js +157 -21
  128. package/build/angular/renderer/index.js.map +1 -1
  129. package/build/angular/services/color-scheme.service.js.map +1 -1
  130. package/build/angular/services/window-dimensions.service.js +1 -1
  131. package/build/angular/services/window-dimensions.service.js.map +1 -1
  132. package/build/angular/state-style.d.ts +9 -0
  133. package/build/angular/state-style.js +39 -0
  134. package/build/angular/state-style.js.map +1 -0
  135. package/metro-transformer.cjs +69 -0
  136. package/package.json +27 -8
  137. package/src/anchor-host-registry.ts +3 -1
  138. package/src/bootstrap.ts +4 -1
  139. package/src/components/activity-indicator/index.android.ts +7 -2
  140. package/src/components/activity-indicator/index.ios.ts +3 -1
  141. package/src/components/activity-indicator/shared.ts +32 -13
  142. package/src/components/button.ts +36 -6
  143. package/src/components/flat-list/index.ts +91 -29
  144. package/src/components/image/index.android.ts +9 -6
  145. package/src/components/image/index.ios.ts +9 -6
  146. package/src/components/image/shared.ts +110 -45
  147. package/src/components/image-background.ts +47 -10
  148. package/src/components/input-accessory-view/index.ts +41 -18
  149. package/src/components/keyboard-avoiding-view/index.ts +76 -22
  150. package/src/components/modal/index.ts +39 -12
  151. package/src/components/pressable/index.ts +121 -31
  152. package/src/components/refresh-control/index.ts +36 -13
  153. package/src/components/safe-area-view/index.ts +44 -19
  154. package/src/components/scroll-view/index.android.ts +31 -9
  155. package/src/components/scroll-view/index.ios.ts +17 -4
  156. package/src/components/scroll-view/projection.ts +290 -63
  157. package/src/components/scroll-view/shared.ts +124 -30
  158. package/src/components/scroll-view/sticky-header.ts +25 -8
  159. package/src/components/section-list/index.ts +79 -19
  160. package/src/components/switch/index.android.ts +18 -7
  161. package/src/components/switch/index.ios.ts +18 -7
  162. package/src/components/switch/shared.ts +15 -4
  163. package/src/components/text-input.ts +119 -31
  164. package/src/components/touchable/index.ts +339 -72
  165. package/src/components/touchable-native-feedback/index.ts +50 -18
  166. package/src/components/virtualized-list/directives.ts +19 -5
  167. package/src/components/virtualized-list/index.ts +292 -72
  168. package/src/components/virtualized-section-list/directives.ts +6 -3
  169. package/src/components/virtualized-section-list/index.ts +164 -34
  170. package/src/components.ts +6 -1
  171. package/src/create-portal/index.ts +47 -11
  172. package/src/create-tunnel/index.ts +3 -1
  173. package/src/descriptor-to-angular/index.ts +29 -8
  174. package/src/diagnostics.ts +138 -0
  175. package/src/gate-demand.ts +109 -0
  176. package/src/host-instance/index.ts +9 -3
  177. package/src/index.ts +33 -2
  178. package/src/modules/animated/create-animated-component.ts +67 -19
  179. package/src/modules/app-registry/index.ts +3 -1
  180. package/src/modules/status-bar/index.ts +13 -4
  181. package/src/primitives/index.ts +73 -2
  182. package/src/primitives/shared.ts +70 -9
  183. package/src/register.ts +43 -0
  184. package/src/render/index.ts +59 -7
  185. package/src/renderer/index.ts +209 -37
  186. package/src/services/color-scheme.service.ts +3 -1
  187. package/src/services/window-dimensions.service.ts +11 -4
  188. package/src/state-style.ts +39 -0
@@ -3,7 +3,16 @@
3
3
  // component imports them and renders them in its template. Declaring `style` as a real
4
4
  // Angular input prevents Angular's CSS style engine from decomposing RN `StyleProp` arrays.
5
5
 
6
- import { Component } from '@angular/core';
6
+ import {
7
+ Component,
8
+ Input,
9
+ type OnInit,
10
+ type SimpleChanges,
11
+ } from '@angular/core';
12
+ import {
13
+ resolveTextProps,
14
+ type IEllipsizeMode,
15
+ } from '@symbiote-native/components';
7
16
  import { SymbiotePrimitiveHost } from './shared';
8
17
 
9
18
  export {
@@ -21,12 +30,47 @@ export {
21
30
  })
22
31
  export class ViewHost extends SymbiotePrimitiveHost {}
23
32
 
33
+ /**
34
+ * Text carries the two defaults RN's Text.js applies unconditionally (`ellipsizeMode ?? 'tail'`,
35
+ * `allowFontScaling !== false`, Text.js:289 and :291). Without them native falls back to `clip` and a
36
+ * clamped Text cuts mid-word with no ellipsis — device-observed on the other adapters 2026-08-19.
37
+ *
38
+ * They are declared as real `@Input()`s, unlike the pass-through props the base's comment
39
+ * describes, for one reason: the default can only be applied by code that can SEE whether the
40
+ * caller supplied a value. A host-binding pass-through is invisible here, so blindly writing
41
+ * 'tail' would silently overwrite an explicit `ellipsizeMode="clip"`.
42
+ */
24
43
  @Component({
25
44
  selector: 'symbiote-text, Text',
26
45
  standalone: true,
27
46
  template: '<ng-content></ng-content>',
28
47
  })
29
- export class TextHost extends SymbiotePrimitiveHost {}
48
+ export class TextHost extends SymbiotePrimitiveHost implements OnInit {
49
+ @Input() ellipsizeMode?: IEllipsizeMode;
50
+ @Input() allowFontScaling?: boolean;
51
+
52
+ // ngOnChanges fires before ngOnInit when a binding exists, and not at all when none does —
53
+ // so both hooks are needed to guarantee the defaults land exactly once per settled value.
54
+ ngOnInit(): void {
55
+ this.applyTextDefaults();
56
+ }
57
+
58
+ override ngOnChanges(changes: SimpleChanges): void {
59
+ super.ngOnChanges(changes);
60
+ if ('ellipsizeMode' in changes || 'allowFontScaling' in changes) {
61
+ this.applyTextDefaults();
62
+ }
63
+ }
64
+
65
+ private applyTextDefaults(): void {
66
+ const resolved = resolveTextProps({
67
+ ellipsizeMode: this.ellipsizeMode,
68
+ allowFontScaling: this.allowFontScaling,
69
+ });
70
+ this.setHostProp('ellipsizeMode', resolved.ellipsizeMode);
71
+ this.setHostProp('allowFontScaling', resolved.allowFontScaling);
72
+ }
73
+ }
30
74
 
31
75
  @Component({
32
76
  selector: 'symbiote-image',
@@ -63,6 +107,9 @@ export class HorizontalScrollView extends SymbiotePrimitiveHost {}
63
107
  })
64
108
  export class HorizontalScrollContentView extends SymbiotePrimitiveHost {}
65
109
 
110
+ // The pair the LOWERED path commits — declared so the tag alphabet stays complete, though no
111
+ // Angular template renders them: this adapter has no lowering transform. The `-managed` pair below
112
+ // is what its own TextInput renders.
66
113
  @Component({
67
114
  selector: 'symbiote-text-input',
68
115
  standalone: true,
@@ -77,6 +124,20 @@ export class TextInputHost extends SymbiotePrimitiveHost {}
77
124
  })
78
125
  export class MultilineTextInputHost extends SymbiotePrimitiveHost {}
79
126
 
127
+ @Component({
128
+ selector: 'symbiote-text-input-managed',
129
+ standalone: true,
130
+ template: '<ng-content></ng-content>',
131
+ })
132
+ export class ManagedTextInputHost extends SymbiotePrimitiveHost {}
133
+
134
+ @Component({
135
+ selector: 'symbiote-text-input-multiline-managed',
136
+ standalone: true,
137
+ template: '<ng-content></ng-content>',
138
+ })
139
+ export class ManagedMultilineTextInputHost extends SymbiotePrimitiveHost {}
140
+
80
141
  @Component({
81
142
  selector: 'symbiote-switch',
82
143
  standalone: true,
@@ -84,6 +145,16 @@ export class MultilineTextInputHost extends SymbiotePrimitiveHost {}
84
145
  })
85
146
  export class SwitchHost extends SymbiotePrimitiveHost {}
86
147
 
148
+ // The component path's spelling — same native Switch/AndroidSwitch, a tag the engine's Switch
149
+ // behavior does not carry. See `component-names/shared.ts` for why the wrapper may not share the
150
+ // lowered tag; mirrors `ManagedTextInputHost` above.
151
+ @Component({
152
+ selector: 'symbiote-switch-managed',
153
+ standalone: true,
154
+ template: '<ng-content></ng-content>',
155
+ })
156
+ export class ManagedSwitchHost extends SymbiotePrimitiveHost {}
157
+
87
158
  @Component({
88
159
  selector: 'symbiote-activity-indicator',
89
160
  standalone: true,
@@ -9,6 +9,7 @@ import {
9
9
  Renderer2,
10
10
  type SimpleChanges,
11
11
  } from '@angular/core';
12
+ import { countAngular } from '../diagnostics';
12
13
  import {
13
14
  flattenStyle,
14
15
  isSymbioteNode,
@@ -61,7 +62,9 @@ function isStyleValue<T>(value: unknown): value is IStyleProp<T> {
61
62
  );
62
63
  }
63
64
 
64
- export function anchorStyleProp<T>(elementRef: ElementRef<unknown>): IStyleProp<T> | undefined {
65
+ export function anchorStyleProp<T>(
66
+ elementRef: ElementRef<unknown>,
67
+ ): IStyleProp<T> | undefined {
65
68
  const value = anchorHostStyle(elementRef);
66
69
  return isStyleValue<T>(value) ? value : undefined;
67
70
  }
@@ -70,7 +73,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
70
73
  return typeof value === 'object' && value !== null;
71
74
  }
72
75
 
73
- function shallowStyleEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean {
76
+ function shallowStyleEqual(
77
+ a: Record<string, unknown>,
78
+ b: Record<string, unknown>,
79
+ ): boolean {
74
80
  const aKeys = Object.keys(a);
75
81
  if (aKeys.length !== Object.keys(b).length) return false;
76
82
  return aKeys.every(key => a[key] === b[key]);
@@ -97,7 +103,9 @@ export function stableAnchorStyle(
97
103
  previous: unknown,
98
104
  ): Record<string, unknown> {
99
105
  const next = flattenStyle([anchorHostStyle(elementRef), explicitStyle]);
100
- return isRecord(previous) && shallowStyleEqual(previous, next) ? previous : next;
106
+ return isRecord(previous) && shallowStyleEqual(previous, next)
107
+ ? previous
108
+ : next;
101
109
  }
102
110
 
103
111
  const ON_PREFIX = /^on[A-Z]/;
@@ -166,9 +174,11 @@ export class SymbioteStyleInputDirective implements DoCheck, OnChanges {
166
174
  // check re-dirties the view every tick and free-runs CD, the hazard `stableAnchorStyle` above
167
175
  // prevents one level up.
168
176
  ngDoCheck(): void {
177
+ countAngular('styleChecks');
169
178
  const current = anchorHostStyle(this.elementRef);
170
179
  if (isAnchorStyleUnchanged(this.lastAnchorStyle, current)) return;
171
180
  this.lastAnchorStyle = current;
181
+ countAngular('styleMarks');
172
182
  this.cdr.markForCheck();
173
183
  }
174
184
  }
@@ -178,7 +188,9 @@ export class SymbioteStyleInputDirective implements DoCheck, OnChanges {
178
188
  // above into a CD loop.
179
189
  function isAnchorStyleUnchanged(previous: unknown, next: unknown): boolean {
180
190
  if (Object.is(previous, next)) return true;
181
- return isRecord(previous) && isRecord(next) && shallowStyleEqual(previous, next);
191
+ return (
192
+ isRecord(previous) && isRecord(next) && shallowStyleEqual(previous, next)
193
+ );
182
194
  }
183
195
 
184
196
  /**
@@ -194,10 +206,17 @@ function isAnchorStyleUnchanged(previous: unknown, next: unknown): boolean {
194
206
  * custom renderer's `setProperty`, so the engine's `routeProp` handles them as usual.
195
207
  */
196
208
  @Directive()
197
- export abstract class SymbiotePrimitiveHost implements OnChanges {
209
+ export abstract class SymbiotePrimitiveHost implements DoCheck, OnChanges {
198
210
  private readonly renderer = inject(Renderer2);
199
211
  private readonly elementRef = inject(ElementRef);
200
212
 
213
+ // Counting only. Angular calls this once per view per check, so it is the only honest measure of
214
+ // how much tree a tick walks - and with CheckAlways primitives that is the whole application, not
215
+ // the screen on top. One integer increment next to a template refresh is not measurable.
216
+ ngDoCheck(): void {
217
+ countAngular('viewsChecked');
218
+ }
219
+
201
220
  @Input() style?: IStyleProp<unknown>;
202
221
 
203
222
  /**
@@ -210,8 +229,18 @@ export abstract class SymbiotePrimitiveHost implements OnChanges {
210
229
 
211
230
  ngOnChanges(changes: SimpleChanges): void {
212
231
  if (!('style' in changes)) return;
213
- const value = this.style === undefined ? undefined : flattenStyle(this.style);
214
- this.renderer.setProperty(this.elementRef.nativeElement, 'style', value);
232
+ const value =
233
+ this.style === undefined ? undefined : flattenStyle(this.style);
234
+ this.setHostProp('style', value);
235
+ }
236
+
237
+ /**
238
+ * Push one resolved prop onto the host element. `protected` rather than exposing the injected
239
+ * Renderer2/ElementRef, so a subclass applying its own defaults (TextHost) uses the same single
240
+ * write path this base already uses for `style`.
241
+ */
242
+ protected setHostProp(key: string, value: unknown): void {
243
+ this.renderer.setProperty(this.elementRef.nativeElement, key, value);
215
244
  }
216
245
  }
217
246
 
@@ -246,16 +275,48 @@ export class SymbioteHostPropsDirective {
246
275
  // Reference stability is the point, not just the allocation: a fresh function on every push
247
276
  // defeats every downstream identity check on the bag, so nothing upstream could ever conclude
248
277
  // "this bag is unchanged" while a callback prop was in it.
249
- private readonly wrappers = new WeakMap<object, (...args: unknown[]) => unknown>();
278
+ private readonly wrappers = new WeakMap<
279
+ object,
280
+ (...args: unknown[]) => unknown
281
+ >();
282
+
283
+ // Values pushed by the previous bag, so a push writes only what actually moved. Copied rather
284
+ // than aliased: a component is free to hand back the same object with mutated fields.
285
+ private pushed: Record<string, unknown> = {};
250
286
 
251
287
  get node(): unknown {
252
288
  return this.elementRef.nativeElement;
253
289
  }
254
290
 
291
+ // MEASURED 2026-08-20, headless 1000-row create: writing every key of every bag cost 104 000
292
+ // engine setProp calls where Solid's identical screen cost 12 000 — and 90 000 of them carried
293
+ // `undefined`. A composed component's bag is a FIXED-SHAPE literal (Pressable's is ~48 keys,
294
+ // most of them unset on any given instance), so the first push of a fresh node spends most of
295
+ // its work deleting keys the node never had. Diffing per key removes exactly that: an unset key
296
+ // is skipped on mount and still written the moment it changes to or from a real value.
255
297
  set symbioteHostProps(props: Record<string, unknown>) {
298
+ const pushed = this.pushed;
299
+ // Only keys the node actually carries, so the vanished-key sweep below walks a handful rather
300
+ // than the bag's whole declared shape.
301
+ const next: Record<string, unknown> = {};
256
302
  for (const [key, value] of Object.entries(props)) {
257
- this.renderer.setProperty(this.elementRef.nativeElement, key, this.wrapCallback(key, value));
303
+ if (value !== undefined) next[key] = value;
304
+ if (Object.is(pushed[key], value)) continue;
305
+ this.renderer.setProperty(
306
+ this.elementRef.nativeElement,
307
+ key,
308
+ this.wrapCallback(key, value),
309
+ );
310
+ }
311
+ // A bag's key SET is not fixed across pushes — resolveAccessibilityProps folds aria-* aliases
312
+ // into a different shape once any of them holds a value — and a key that vanishes must clear
313
+ // the prop rather than leave the last value on the native view. A key still IN the bag was
314
+ // already handled above, `undefined` included.
315
+ for (const key of Object.keys(pushed)) {
316
+ if (key in props) continue;
317
+ this.renderer.setProperty(this.elementRef.nativeElement, key, undefined);
258
318
  }
319
+ this.pushed = next;
259
320
  }
260
321
 
261
322
  // A flat-bag `onX` callback (responder negotiation, onLongPress, …) is invoked by the engine's
@@ -0,0 +1,43 @@
1
+ // Side-effect ONLY. Exports nothing, and must never be re-exported from the barrel.
2
+ //
3
+ // A lowered `<symbiote-pressable>`/`<symbiote-text-input>` is a bare custom element with no
4
+ // component behind it, so the press machine and the controlled-input machine have to live on the
5
+ // engine node itself — reached by `registerPressableBehavior()`/`registerTextInputBehavior()`
6
+ // having run before the first node of that tag is created.
7
+ //
8
+ // Metro turns on `inlineRequires` for PRODUCTION only, moving a `require()` down to the first place
9
+ // its binding is used as a VALUE; a barrel's `export { X } from './x'` compiles to a lazy getter,
10
+ // so a module reached only that way never evaluates unless something names it as a value — silent
11
+ // in Release builds alone, invisible to tsc, to vitest, and to grepping the bundle (see CLAUDE.md,
12
+ // "Never make correctness depend on a module's load-time side effect"). A bare
13
+ // `import './register';` placed NEXT TO a re-export of the same specifier does not help either —
14
+ // Babel merges the two into one dependency and the merged one stays lazy. This is the shape that
15
+ // survives both traps, matching `adapters/{vue,svelte,solid}/src/register.ts` and
16
+ // `packages/slider/src/{react,vue,svelte,angular}/index.ts`.
17
+ import {
18
+ registerImageBehavior,
19
+ registerInputAccessoryViewBehavior,
20
+ registerPressableBehavior,
21
+ registerSwitchBehavior,
22
+ registerTextInputBehavior,
23
+ } from '@symbiote-native/components';
24
+
25
+ registerPressableBehavior();
26
+ // Only the LOWERED tags carry this — the wrapper renders `symbiote-text-input-managed` /
27
+ // `symbiote-text-input-multiline-managed` and keeps running its own lifecycle. One owner per node;
28
+ // see `component-names/shared.ts`.
29
+ registerTextInputBehavior();
30
+ // Same reason as TextInput: the wrapper renders `symbiote-switch-managed` and runs its own
31
+ // lastNativeReport/snap-back lifecycle, so the engine's copy attaches only to the bare tag.
32
+ registerSwitchBehavior();
33
+
34
+ // Image owns no runtime — its behavior is a prop FOLD and nothing else, and it is registered on the
35
+ // same `symbiote-image` the wrapper already emits rather than on a `-managed` twin. That is safe
36
+ // only because the mapping is idempotent, which `core/components/src/behaviors/image.test.ts`
37
+ // asserts rather than assumes; a wrapper-built node simply folds a second time and nothing moves.
38
+ registerImageBehavior();
39
+
40
+ // Fold-only, and it shares the wrapper's tag for the same reason Image does: the mapping has no
41
+ // aliasing at all, so a wrapper-built node folding a second time moves nothing.
42
+ // `core/components/src/behaviors/input-accessory-view.test.ts` asserts that rather than assuming it.
43
+ registerInputAccessoryViewBehavior();
@@ -10,6 +10,7 @@ import {
10
10
  createSurface,
11
11
  disposeRoot,
12
12
  dlog,
13
+ reportUncaughtError,
13
14
  toPublicInstance,
14
15
  type ISymbioteNode,
15
16
  type IRootTag,
@@ -61,8 +62,38 @@ function asAngularHost(hostNode: SymbioteSurface | ISymbioteNode): Element {
61
62
  // that isn't one of our primitives), so the root needs an explicit host too, one we then hand
62
63
  // to the wrapper as projectable content.
63
64
  function createDetachedViewHost(): ISymbioteNode {
64
- const descriptor = descriptorFor('View');
65
- return toPublicInstance(createEngineElement(descriptor.component, descriptor.isText));
65
+ // 'symbiote-view', not 'View': the public name is not a Fabric view name, and until
66
+ // makeDescriptorFor learned to reject one it fell through to a view literally named `View`.
67
+ const descriptor = descriptorFor('symbiote-view');
68
+ return toPublicInstance(
69
+ createEngineElement(descriptor.component, descriptor.isText),
70
+ );
71
+ }
72
+
73
+ // Angular's whole unhandled-error funnel: INTERNAL_APPLICATION_ERROR_HANDLER resolves this token
74
+ // and hands it everything the framework caught on the app's behalf — a scheduled (async) tick that
75
+ // threw, a template listener that threw, a rejected pending task. Routed to the engine so an
76
+ // Angular app reaches the same native redbox as a React one instead of a console line nobody sees
77
+ // off a dev machine.
78
+ //
79
+ // There is deliberately no caught-vs-uncaught split here, unlike the React adapter's
80
+ // onUncaughtError/onCaughtError pair. Angular has no error boundary, and every path where the app
81
+ // DID handle something keeps clear of this seam on its own: a `@defer` block with an `@error`
82
+ // branch renders that branch (defer/rendering.ts calls handleUncaughtError only when there is
83
+ // none), `resource()` parks the failure in its own error signal, and an app-level try/catch or
84
+ // `catchError` never reaches Angular at all. So anything arriving here is by construction
85
+ // unclaimed, and the redbox is the right answer for all of it.
86
+ //
87
+ // Two things this must not do. It must not also call `super.handleError` — reportUncaughtError
88
+ // picks exactly one channel (host reporter or console) precisely because RN routes console.error
89
+ // into LogBox too, so the pair would double-report every error. And it must not throw: the
90
+ // scheduler invokes this from inside its own `catch` (zoneless_scheduling_impl.ts's tick), where a
91
+ // second throw escapes to the host and kills the "report and keep running" behaviour the provider
92
+ // exists for.
93
+ class SymbioteErrorHandler extends ErrorHandler {
94
+ override handleError(error: unknown): void {
95
+ reportUncaughtError(error, { origin: 'angular render' });
96
+ }
66
97
  }
67
98
 
68
99
  interface IMountedApp {
@@ -81,7 +112,10 @@ export interface IMountOptions {
81
112
  wrapperComponent?: Type<unknown>;
82
113
  }
83
114
 
84
- function applyInputs(cmpRef: ComponentRef<unknown>, initialProps: object | undefined): void {
115
+ function applyInputs(
116
+ cmpRef: ComponentRef<unknown>,
117
+ initialProps: object | undefined,
118
+ ): void {
85
119
  if (initialProps === undefined) return;
86
120
  for (const [key, value] of Object.entries(initialProps)) {
87
121
  cmpRef.setInput(key, value);
@@ -118,7 +152,23 @@ export function mount(
118
152
  provide: RendererFactory2,
119
153
  useValue: new SymbioteRendererFactory(surface),
120
154
  },
121
- { provide: DOCUMENT, useValue: { head: surface, body: surface } },
155
+ // `getElementById` is not decoration: `resource()` resolves TransferState, whose root
156
+ // factory runs `retrieveTransferredState(doc, appId)` ->
157
+ // `doc.getElementById(appId + '-state')` (core/src/transfer_state.ts:156) to pick up
158
+ // server-rendered state. A stub without it makes that a TypeError, and because the throw
159
+ // happens while the component is being constructed, the ENTIRE screen renders nothing —
160
+ // white body under a native header that navigation drew anyway, no error surfaced. The
161
+ // `optional: true` on the injector lookup does not help: the token resolves, its factory
162
+ // is what throws. Returning null is the honest client answer — there is no server here, so
163
+ // `script?.tagName` short-circuits and the caller gets `{}`.
164
+ {
165
+ provide: DOCUMENT,
166
+ useValue: {
167
+ head: surface,
168
+ body: surface,
169
+ getElementById: (): null => null,
170
+ },
171
+ },
122
172
  // createEnvironmentInjector with a null parent scopes this injector to {'environment'}
123
173
  // only (see EnvironmentNgModuleRefAdapter), so providedIn:'root' tokens — ApplicationRef
124
174
  // included — never resolve (R3Injector.get walks up for a `scopes` containing 'root', and
@@ -142,9 +192,11 @@ export function mount(
142
192
  // `injector.get(ErrorHandler)`; a normal `bootstrapApplication` registers that token by
143
193
  // default, but our from-scratch environment injector never did, so the lookup itself threw
144
194
  // NG0201 and replaced the real error with "No provider found for ErrorHandler" — any async
145
- // tick() exception crashed hard, uncaught, instead of being reported. Providing the
146
- // default `ErrorHandler` restores `console.error('ERROR', e)` and keep running.
147
- { provide: ErrorHandler, useClass: ErrorHandler },
195
+ // tick() exception crashed hard, uncaught, instead of being reported. The token still has to
196
+ // be provided for exactly that reason; what it resolves to is now SymbioteErrorHandler
197
+ // (see above), which reports through the engine instead of Angular's `console.error('ERROR',
198
+ // e)` and, like the default, returns rather than rethrows so the app keeps running.
199
+ { provide: ErrorHandler, useClass: SymbioteErrorHandler },
148
200
  ColorSchemeService,
149
201
  WindowDimensionsService,
150
202
  ],