@symbiote-native/angular 0.7.0 → 0.8.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 (158) hide show
  1. package/README.md +3 -3
  2. package/build/angular/bootstrap.js +1 -1
  3. package/build/angular/bootstrap.js.map +1 -1
  4. package/build/angular/components/activity-indicator/index.android.js +7 -2
  5. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  6. package/build/angular/components/activity-indicator/index.ios.js +3 -1
  7. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  8. package/build/angular/components/activity-indicator/shared.d.ts +1 -1
  9. package/build/angular/components/activity-indicator/shared.js +1 -1
  10. package/build/angular/components/activity-indicator/shared.js.map +1 -1
  11. package/build/angular/components/button.js +13 -5
  12. package/build/angular/components/button.js.map +1 -1
  13. package/build/angular/components/flat-list/index.d.ts +1 -1
  14. package/build/angular/components/flat-list/index.js +47 -14
  15. package/build/angular/components/flat-list/index.js.map +1 -1
  16. package/build/angular/components/image/index.android.js +4 -2
  17. package/build/angular/components/image/index.android.js.map +1 -1
  18. package/build/angular/components/image/index.ios.js +4 -2
  19. package/build/angular/components/image/index.ios.js.map +1 -1
  20. package/build/angular/components/image/shared.js +18 -6
  21. package/build/angular/components/image/shared.js.map +1 -1
  22. package/build/angular/components/image-background.js +22 -8
  23. package/build/angular/components/image-background.js.map +1 -1
  24. package/build/angular/components/input-accessory-view/index.js +7 -2
  25. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  26. package/build/angular/components/keyboard-avoiding-view/index.d.ts +1 -0
  27. package/build/angular/components/keyboard-avoiding-view/index.js +39 -9
  28. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  29. package/build/angular/components/modal/index.js +7 -2
  30. package/build/angular/components/modal/index.js.map +1 -1
  31. package/build/angular/components/pressable/index.d.ts +1 -1
  32. package/build/angular/components/pressable/index.js +6 -2
  33. package/build/angular/components/pressable/index.js.map +1 -1
  34. package/build/angular/components/refresh-control/index.js +3 -1
  35. package/build/angular/components/refresh-control/index.js.map +1 -1
  36. package/build/angular/components/safe-area-view/index.js +3 -1
  37. package/build/angular/components/safe-area-view/index.js.map +1 -1
  38. package/build/angular/components/scroll-view/index.android.js +44 -14
  39. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  40. package/build/angular/components/scroll-view/index.ios.js +13 -5
  41. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  42. package/build/angular/components/scroll-view/projection.d.ts +32 -0
  43. package/build/angular/components/scroll-view/projection.js +212 -47
  44. package/build/angular/components/scroll-view/projection.js.map +1 -1
  45. package/build/angular/components/scroll-view/shared.js +28 -7
  46. package/build/angular/components/scroll-view/shared.js.map +1 -1
  47. package/build/angular/components/scroll-view/sticky-header.js +7 -3
  48. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  49. package/build/angular/components/section-list/index.d.ts +6 -1
  50. package/build/angular/components/section-list/index.js +63 -18
  51. package/build/angular/components/section-list/index.js.map +1 -1
  52. package/build/angular/components/switch/index.android.js +17 -3
  53. package/build/angular/components/switch/index.android.js.map +1 -1
  54. package/build/angular/components/switch/index.ios.js +17 -3
  55. package/build/angular/components/switch/index.ios.js.map +1 -1
  56. package/build/angular/components/switch/shared.d.ts +1 -1
  57. package/build/angular/components/switch/shared.js +5 -2
  58. package/build/angular/components/switch/shared.js.map +1 -1
  59. package/build/angular/components/text-input.d.ts +1 -1
  60. package/build/angular/components/text-input.js +25 -5
  61. package/build/angular/components/text-input.js.map +1 -1
  62. package/build/angular/components/touchable/index.d.ts +29 -7
  63. package/build/angular/components/touchable/index.js +246 -69
  64. package/build/angular/components/touchable/index.js.map +1 -1
  65. package/build/angular/components/touchable-native-feedback/index.js +2 -2
  66. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  67. package/build/angular/components/virtualized-list/directives.js +4 -0
  68. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  69. package/build/angular/components/virtualized-list/index.d.ts +5 -2
  70. package/build/angular/components/virtualized-list/index.js +197 -60
  71. package/build/angular/components/virtualized-list/index.js.map +1 -1
  72. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  73. package/build/angular/components/virtualized-section-list/index.d.ts +18 -1
  74. package/build/angular/components/virtualized-section-list/index.js +90 -21
  75. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  76. package/build/angular/components.d.ts +1 -1
  77. package/build/angular/components.js.map +1 -1
  78. package/build/angular/create-portal/index.js +5 -1
  79. package/build/angular/create-portal/index.js.map +1 -1
  80. package/build/angular/create-tunnel/index.js.map +1 -1
  81. package/build/angular/descriptor-to-angular/index.js +3 -1
  82. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  83. package/build/angular/diagnostics.d.ts +33 -0
  84. package/build/angular/diagnostics.js +82 -0
  85. package/build/angular/diagnostics.js.map +1 -0
  86. package/build/angular/host-instance/index.js +1 -1
  87. package/build/angular/host-instance/index.js.map +1 -1
  88. package/build/angular/index.d.ts +5 -2
  89. package/build/angular/index.js +15 -1
  90. package/build/angular/index.js.map +1 -1
  91. package/build/angular/modules/animated/create-animated-component.js +27 -9
  92. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  93. package/build/angular/modules/app-registry/index.js.map +1 -1
  94. package/build/angular/modules/status-bar/index.js +1 -1
  95. package/build/angular/modules/status-bar/index.js.map +1 -1
  96. package/build/angular/primitives/index.d.ts +19 -2
  97. package/build/angular/primitives/index.js +39 -3
  98. package/build/angular/primitives/index.js.map +1 -1
  99. package/build/angular/primitives/shared.d.ts +9 -1
  100. package/build/angular/primitives/shared.js +49 -3
  101. package/build/angular/primitives/shared.js.map +1 -1
  102. package/build/angular/render/index.js +48 -5
  103. package/build/angular/render/index.js.map +1 -1
  104. package/build/angular/renderer/index.d.ts +1 -0
  105. package/build/angular/renderer/index.js +37 -2
  106. package/build/angular/renderer/index.js.map +1 -1
  107. package/build/angular/services/color-scheme.service.js.map +1 -1
  108. package/build/angular/services/window-dimensions.service.js +1 -1
  109. package/build/angular/services/window-dimensions.service.js.map +1 -1
  110. package/package.json +5 -5
  111. package/src/bootstrap.ts +4 -1
  112. package/src/components/activity-indicator/index.android.ts +7 -2
  113. package/src/components/activity-indicator/index.ios.ts +3 -1
  114. package/src/components/activity-indicator/shared.ts +32 -13
  115. package/src/components/button.ts +19 -6
  116. package/src/components/flat-list/index.ts +91 -29
  117. package/src/components/image/index.android.ts +9 -2
  118. package/src/components/image/index.ios.ts +9 -2
  119. package/src/components/image/shared.ts +58 -19
  120. package/src/components/image-background.ts +29 -10
  121. package/src/components/input-accessory-view/index.ts +27 -10
  122. package/src/components/keyboard-avoiding-view/index.ts +55 -15
  123. package/src/components/modal/index.ts +22 -8
  124. package/src/components/pressable/index.ts +51 -21
  125. package/src/components/refresh-control/index.ts +22 -8
  126. package/src/components/safe-area-view/index.ts +16 -7
  127. package/src/components/scroll-view/index.android.ts +31 -9
  128. package/src/components/scroll-view/index.ios.ts +17 -4
  129. package/src/components/scroll-view/projection.ts +290 -63
  130. package/src/components/scroll-view/shared.ts +93 -26
  131. package/src/components/scroll-view/sticky-header.ts +25 -8
  132. package/src/components/section-list/index.ts +61 -19
  133. package/src/components/switch/index.android.ts +14 -3
  134. package/src/components/switch/index.ios.ts +14 -3
  135. package/src/components/switch/shared.ts +15 -4
  136. package/src/components/text-input.ts +45 -13
  137. package/src/components/touchable/index.ts +296 -72
  138. package/src/components/touchable-native-feedback/index.ts +32 -10
  139. package/src/components/virtualized-list/directives.ts +19 -5
  140. package/src/components/virtualized-list/index.ts +274 -72
  141. package/src/components/virtualized-section-list/directives.ts +6 -3
  142. package/src/components/virtualized-section-list/index.ts +146 -34
  143. package/src/components.ts +4 -1
  144. package/src/create-portal/index.ts +8 -2
  145. package/src/create-tunnel/index.ts +3 -1
  146. package/src/descriptor-to-angular/index.ts +29 -8
  147. package/src/diagnostics.ts +138 -0
  148. package/src/host-instance/index.ts +9 -3
  149. package/src/index.ts +31 -2
  150. package/src/modules/animated/create-animated-component.ts +44 -15
  151. package/src/modules/app-registry/index.ts +3 -1
  152. package/src/modules/status-bar/index.ts +13 -4
  153. package/src/primitives/index.ts +46 -2
  154. package/src/primitives/shared.ts +70 -9
  155. package/src/render/index.ts +56 -6
  156. package/src/renderer/index.ts +76 -14
  157. package/src/services/color-scheme.service.ts +3 -1
  158. package/src/services/window-dimensions.service.ts +11 -4
package/src/index.ts CHANGED
@@ -61,6 +61,16 @@ export {
61
61
  AnimatedText,
62
62
  AnimatedView,
63
63
  } from './modules/animated';
64
+ // Exported even though Angular's version maps only the four primitives (View/Text/Image/
65
+ // ScrollView) onto the pre-authored wrappers above and THROWS on anything else — there is no JIT
66
+ // under AOT/Metro, so it cannot synthesize a wrapper at runtime the way the other four adapters
67
+ // do. It ships anyway because portable code (`createAnimatedComponent(View)`) then compiles and
68
+ // runs identically on all five adapters, and the one case Angular cannot serve fails with a
69
+ // message naming the fix (author a standalone @Component over AnimatedComponentBase) instead of
70
+ // failing as a missing export, which names nothing. Found by the barrel audit in
71
+ // `.claude/rules/adapter-parity-audit.md`: it was implemented and tested here all along and simply
72
+ // never reached this file, so `@symbiote-native/angular` consumers could not reach it at all.
73
+ export { createAnimatedComponent } from './modules/animated';
64
74
  export type {
65
75
  IActivityIndicatorProps,
66
76
  IAngularImageBackgroundProps,
@@ -135,7 +145,12 @@ export { DescriptorOutlet } from './descriptor-to-angular';
135
145
  // idiom) parameterized by an `@Input()`, rather than a factory returning fresh components per
136
146
  // call.
137
147
  export { PortalDirective, PortalOutletDirective } from './create-portal';
138
- export { createTunnel, TunnelInDirective, TunnelOut, type ITunnelStore } from './create-tunnel';
148
+ export {
149
+ createTunnel,
150
+ TunnelInDirective,
151
+ TunnelOut,
152
+ type ITunnelStore,
153
+ } from './create-tunnel';
139
154
  export { SymbioteRenderer, SymbioteRendererFactory } from './renderer';
140
155
  // registerComposedComponent lives in the dependency-free leaf ./anchor-host-registry, NOT in the
141
156
  // require-cyclic ./renderer — see the leaf header. The babel-register-composed plugin injects the
@@ -258,7 +273,11 @@ export {
258
273
  setNativeViewConfigSource,
259
274
  StyleSheet,
260
275
  } from '@symbiote-native/engine';
261
- export type { IRootTag, ISymbioteEvent, ISymbioteNode } from '@symbiote-native/engine';
276
+ export type {
277
+ IRootTag,
278
+ ISymbioteEvent,
279
+ ISymbioteNode,
280
+ } from '@symbiote-native/engine';
262
281
  // The agnostic style / platform / view-config types behind the values above. Angular takes its
263
282
  // component props as @Input()s and exports no per-component prop type, but an app still needs
264
283
  // these to type a style object or a Platform.select spec.
@@ -288,3 +307,13 @@ export type {
288
307
  IPressableAndroidRippleConfig,
289
308
  IImageStatics,
290
309
  } from '@symbiote-native/components';
310
+
311
+ // Diagnostics: per-window Angular-side counters (change-detection passes, renderer writes, list
312
+ // recomputes, cell views). The engine's readCommitProfile() prices the commit; these say how many
313
+ // times anything ran at all - the number that exposes a change-detection free-run.
314
+ export {
315
+ readAngularProfile,
316
+ readAngularProfileDetail,
317
+ setAngularProfileDetail,
318
+ } from './diagnostics';
319
+ export type { IAngularProfile, IAngularProfileDetail } from './diagnostics';
@@ -69,7 +69,11 @@ const PASSTHROUGH_PROP = 'passthroughAnimatedPropExplicitValues';
69
69
  // plus static host props to forward (the React/Vue `rest`). Each concrete @Component re-lists
70
70
  // these (Angular's inputs-on-base convention, mirroring Switch/Image).
71
71
  export const ANIMATED_INPUTS = ['style', 'animatedProps', PASSTHROUGH_PROP];
72
- export const ANIMATED_IMAGE_INPUTS = [...IMAGE_INPUTS, 'animatedProps', PASSTHROUGH_PROP];
72
+ export const ANIMATED_IMAGE_INPUTS = [
73
+ ...IMAGE_INPUTS,
74
+ 'animatedProps',
75
+ PASSTHROUGH_PROP,
76
+ ];
73
77
 
74
78
  // The shared lifecycle for every animated wrapper. Concrete subclasses add ONLY a @Component
75
79
  // decorator (selector + the host-primitive template) — no behavior. Plain fields (no @Input
@@ -92,7 +96,8 @@ export abstract class AnimatedComponentBase
92
96
 
93
97
  // The directive sitting on the inner host primitive; its `node` is the committed SymbioteNode
94
98
  // the leaf binds to. Inherited by the decorated subclass (Angular collects base-class queries).
95
- @ViewChild(SymbioteHostPropsDirective) private hostDirective?: SymbioteHostPropsDirective;
99
+ @ViewChild(SymbioteHostPropsDirective)
100
+ private hostDirective?: SymbioteHostPropsDirective;
96
101
 
97
102
  // The leaf-lifecycle orchestration (build/attach/swap/bind/detach an AnimatedProps leaf) is a
98
103
  // Pure Fabrication shared with AnimatedImage, which cannot extend this class (it must extend
@@ -110,10 +115,14 @@ export abstract class AnimatedComponentBase
110
115
  // array, which the commit layer flattens) so the ShadowTree carries the current transform.
111
116
  get reducedProps(): Record<string, unknown> {
112
117
  const reduced = reduceProps(this.mergedProps());
113
- const passthroughStyle = readPassthroughStyle(this.passthroughAnimatedPropExplicitValues);
118
+ const passthroughStyle = readPassthroughStyle(
119
+ this.passthroughAnimatedPropExplicitValues,
120
+ );
114
121
  if (passthroughStyle !== undefined) {
115
122
  reduced['style'] =
116
- reduced['style'] === undefined ? passthroughStyle : [reduced['style'], passthroughStyle];
123
+ reduced['style'] === undefined
124
+ ? passthroughStyle
125
+ : [reduced['style'], passthroughStyle];
117
126
  }
118
127
  reduced['style'] = [this.anchorStyle(), reduced['style']];
119
128
  return reduced;
@@ -162,7 +171,8 @@ export abstract class AnimatedComponentBase
162
171
  const hasPassthroughAnimatedValues =
163
172
  this.passthroughAnimatedPropExplicitValues !== null &&
164
173
  this.passthroughAnimatedPropExplicitValues !== undefined;
165
- const wantsNative = hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
174
+ const wantsNative =
175
+ hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
166
176
  this.leafBinder.reconcile(props, wantsNative);
167
177
  }
168
178
 
@@ -171,7 +181,9 @@ export abstract class AnimatedComponentBase
171
181
  // never disagree.
172
182
  private mergedProps(): Record<string, unknown> {
173
183
  const base = this.animatedProps ?? {};
174
- return this.style === undefined ? { ...base } : { ...base, style: this.style };
184
+ return this.style === undefined
185
+ ? { ...base }
186
+ : { ...base, style: this.style };
175
187
  }
176
188
 
177
189
  // The committed host node held by IDENTITY. resolveHostNode unwraps an imperative scroll
@@ -186,7 +198,9 @@ export abstract class AnimatedComponentBase
186
198
  @Component({
187
199
  selector: 'AnimatedView, symbiote-animated-view',
188
200
  standalone: true,
189
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
201
+ hostDirectives: [
202
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
203
+ ],
190
204
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
191
205
  imports: [SymbioteHostPropsDirective, ViewHost],
192
206
  inputs: ANIMATED_INPUTS,
@@ -202,7 +216,9 @@ export class AnimatedView extends AnimatedComponentBase {}
202
216
  @Component({
203
217
  selector: 'AnimatedText, symbiote-animated-text',
204
218
  standalone: true,
205
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
219
+ hostDirectives: [
220
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
221
+ ],
206
222
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
207
223
  imports: [SymbioteHostPropsDirective, TextHost],
208
224
  inputs: ANIMATED_INPUTS,
@@ -218,7 +234,9 @@ export class AnimatedText extends AnimatedComponentBase {}
218
234
  @Component({
219
235
  selector: 'AnimatedImage, symbiote-animated-image',
220
236
  standalone: true,
221
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
237
+ hostDirectives: [
238
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
239
+ ],
222
240
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
223
241
  imports: [SymbioteHostPropsDirective, ImageHost],
224
242
  inputs: ANIMATED_IMAGE_INPUTS,
@@ -242,7 +260,10 @@ export class AnimatedText extends AnimatedComponentBase {}
242
260
  `,
243
261
  changeDetection: ChangeDetectionStrategy.OnPush,
244
262
  })
245
- export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges, OnDestroy {
263
+ export class AnimatedImage
264
+ extends ImageBase
265
+ implements AfterViewInit, OnChanges, OnDestroy
266
+ {
246
267
  animatedProps: Record<string, unknown> | undefined;
247
268
  passthroughAnimatedPropExplicitValues: unknown;
248
269
 
@@ -250,7 +271,8 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
250
271
  // comment on AnimatedComponentBase.elementRef.
251
272
  private readonly elementRef = inject(ElementRef);
252
273
 
253
- @ViewChild(SymbioteHostPropsDirective) private hostDirective?: SymbioteHostPropsDirective;
274
+ @ViewChild(SymbioteHostPropsDirective)
275
+ private hostDirective?: SymbioteHostPropsDirective;
254
276
 
255
277
  // Same Pure Fabrication AnimatedComponentBase holds — see its matching comment. AnimatedImage
256
278
  // can't extend AnimatedComponentBase (it must extend ImageBase, see the file header), so it
@@ -263,10 +285,14 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
263
285
 
264
286
  get animatedImageProps(): Record<string, unknown> {
265
287
  const reduced = reduceProps(this.mergedProps());
266
- const passthroughStyle = readPassthroughStyle(this.passthroughAnimatedPropExplicitValues);
288
+ const passthroughStyle = readPassthroughStyle(
289
+ this.passthroughAnimatedPropExplicitValues,
290
+ );
267
291
  if (passthroughStyle !== undefined) {
268
292
  reduced['style'] =
269
- reduced['style'] === undefined ? passthroughStyle : [reduced['style'], passthroughStyle];
293
+ reduced['style'] === undefined
294
+ ? passthroughStyle
295
+ : [reduced['style'], passthroughStyle];
270
296
  }
271
297
  const resolved = resolveImageProps(reduced);
272
298
  resolved['style'] = [anchorHostStyle(this.elementRef), resolved['style']];
@@ -311,7 +337,8 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
311
337
  const hasPassthroughAnimatedValues =
312
338
  this.passthroughAnimatedPropExplicitValues !== null &&
313
339
  this.passthroughAnimatedPropExplicitValues !== undefined;
314
- const wantsNative = hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
340
+ const wantsNative =
341
+ hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
315
342
  this.leafBinder.reconcile(props, wantsNative);
316
343
  }
317
344
 
@@ -332,7 +359,9 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
332
359
  @Component({
333
360
  selector: 'AnimatedScrollView, symbiote-animated-scroll-view',
334
361
  standalone: true,
335
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
362
+ hostDirectives: [
363
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
364
+ ],
336
365
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
337
366
  imports: [SymbioteHostPropsDirective, ScrollViewHost],
338
367
  inputs: ANIMATED_INPUTS,
@@ -23,7 +23,9 @@ export type IComponentProvider = () => Type<unknown>;
23
23
  // a pre-authored standalone @Component whose template renders `<ng-content>` — the Angular idiom
24
24
  // for "render my children" (mount() projects the root's host node into it, the twin of React's
25
25
  // createElement(Wrapper, null, rootElement)).
26
- export type IWrapperComponentProvider = (appParameters: IAppParameters) => Type<unknown>;
26
+ export type IWrapperComponentProvider = (
27
+ appParameters: IAppParameters,
28
+ ) => Type<unknown>;
27
29
 
28
30
  function runnableFor(
29
31
  componentProvider: IComponentProvider,
@@ -5,7 +5,12 @@
5
5
  // ngOnChanges on mount + every prop change. Imperative statics are attached to the component
6
6
  // object the same way RN does.
7
7
 
8
- import { Component, Input, type OnChanges, type SimpleChanges } from '@angular/core';
8
+ import {
9
+ Component,
10
+ Input,
11
+ type OnChanges,
12
+ type SimpleChanges,
13
+ } from '@angular/core';
9
14
  import {
10
15
  applyStatusBarProps,
11
16
  statusBarImperative,
@@ -46,12 +51,16 @@ class StatusBarComponent implements OnChanges {
46
51
  }
47
52
  }
48
53
 
49
- const StatusBarWithStatics = Object.assign(StatusBarComponent, statusBarImperative);
54
+ const StatusBarWithStatics = Object.assign(
55
+ StatusBarComponent,
56
+ statusBarImperative,
57
+ );
50
58
 
51
59
  Object.defineProperty(StatusBarWithStatics, 'currentHeight', {
52
60
  get: statusBarCurrentHeight,
53
61
  enumerable: true,
54
62
  });
55
63
 
56
- export const StatusBar: typeof StatusBarWithStatics & { readonly currentHeight?: number } =
57
- StatusBarWithStatics;
64
+ export const StatusBar: typeof StatusBarWithStatics & {
65
+ readonly currentHeight?: number;
66
+ } = StatusBarWithStatics;
@@ -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:288-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',
@@ -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
@@ -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,
@@ -62,7 +63,35 @@ function asAngularHost(hostNode: SymbioteSurface | ISymbioteNode): Element {
62
63
  // to the wrapper as projectable content.
63
64
  function createDetachedViewHost(): ISymbioteNode {
64
65
  const descriptor = descriptorFor('View');
65
- return toPublicInstance(createEngineElement(descriptor.component, descriptor.isText));
66
+ return toPublicInstance(
67
+ createEngineElement(descriptor.component, descriptor.isText),
68
+ );
69
+ }
70
+
71
+ // Angular's whole unhandled-error funnel: INTERNAL_APPLICATION_ERROR_HANDLER resolves this token
72
+ // and hands it everything the framework caught on the app's behalf — a scheduled (async) tick that
73
+ // threw, a template listener that threw, a rejected pending task. Routed to the engine so an
74
+ // Angular app reaches the same native redbox as a React one instead of a console line nobody sees
75
+ // off a dev machine.
76
+ //
77
+ // There is deliberately no caught-vs-uncaught split here, unlike the React adapter's
78
+ // onUncaughtError/onCaughtError pair. Angular has no error boundary, and every path where the app
79
+ // DID handle something keeps clear of this seam on its own: a `@defer` block with an `@error`
80
+ // branch renders that branch (defer/rendering.ts calls handleUncaughtError only when there is
81
+ // none), `resource()` parks the failure in its own error signal, and an app-level try/catch or
82
+ // `catchError` never reaches Angular at all. So anything arriving here is by construction
83
+ // unclaimed, and the redbox is the right answer for all of it.
84
+ //
85
+ // Two things this must not do. It must not also call `super.handleError` — reportUncaughtError
86
+ // picks exactly one channel (host reporter or console) precisely because RN routes console.error
87
+ // into LogBox too, so the pair would double-report every error. And it must not throw: the
88
+ // scheduler invokes this from inside its own `catch` (zoneless_scheduling_impl.ts's tick), where a
89
+ // second throw escapes to the host and kills the "report and keep running" behaviour the provider
90
+ // exists for.
91
+ class SymbioteErrorHandler extends ErrorHandler {
92
+ override handleError(error: unknown): void {
93
+ reportUncaughtError(error, { origin: 'angular render' });
94
+ }
66
95
  }
67
96
 
68
97
  interface IMountedApp {
@@ -81,7 +110,10 @@ export interface IMountOptions {
81
110
  wrapperComponent?: Type<unknown>;
82
111
  }
83
112
 
84
- function applyInputs(cmpRef: ComponentRef<unknown>, initialProps: object | undefined): void {
113
+ function applyInputs(
114
+ cmpRef: ComponentRef<unknown>,
115
+ initialProps: object | undefined,
116
+ ): void {
85
117
  if (initialProps === undefined) return;
86
118
  for (const [key, value] of Object.entries(initialProps)) {
87
119
  cmpRef.setInput(key, value);
@@ -118,7 +150,23 @@ export function mount(
118
150
  provide: RendererFactory2,
119
151
  useValue: new SymbioteRendererFactory(surface),
120
152
  },
121
- { provide: DOCUMENT, useValue: { head: surface, body: surface } },
153
+ // `getElementById` is not decoration: `resource()` resolves TransferState, whose root
154
+ // factory runs `retrieveTransferredState(doc, appId)` ->
155
+ // `doc.getElementById(appId + '-state')` (core/src/transfer_state.ts:156) to pick up
156
+ // server-rendered state. A stub without it makes that a TypeError, and because the throw
157
+ // happens while the component is being constructed, the ENTIRE screen renders nothing —
158
+ // white body under a native header that navigation drew anyway, no error surfaced. The
159
+ // `optional: true` on the injector lookup does not help: the token resolves, its factory
160
+ // is what throws. Returning null is the honest client answer — there is no server here, so
161
+ // `script?.tagName` short-circuits and the caller gets `{}`.
162
+ {
163
+ provide: DOCUMENT,
164
+ useValue: {
165
+ head: surface,
166
+ body: surface,
167
+ getElementById: (): null => null,
168
+ },
169
+ },
122
170
  // createEnvironmentInjector with a null parent scopes this injector to {'environment'}
123
171
  // only (see EnvironmentNgModuleRefAdapter), so providedIn:'root' tokens — ApplicationRef
124
172
  // included — never resolve (R3Injector.get walks up for a `scopes` containing 'root', and
@@ -142,9 +190,11 @@ export function mount(
142
190
  // `injector.get(ErrorHandler)`; a normal `bootstrapApplication` registers that token by
143
191
  // default, but our from-scratch environment injector never did, so the lookup itself threw
144
192
  // 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 },
193
+ // tick() exception crashed hard, uncaught, instead of being reported. The token still has to
194
+ // be provided for exactly that reason; what it resolves to is now SymbioteErrorHandler
195
+ // (see above), which reports through the engine instead of Angular's `console.error('ERROR',
196
+ // e)` and, like the default, returns rather than rethrows so the app keeps running.
197
+ { provide: ErrorHandler, useClass: SymbioteErrorHandler },
148
198
  ColorSchemeService,
149
199
  WindowDimensionsService,
150
200
  ],