@symbiote-native/angular 0.6.1 → 0.7.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 (206) hide show
  1. package/README.md +1 -1
  2. package/bin/symbiote-angular-dev.cjs +20 -19
  3. package/build/angular/anchor-host-registry.js +2 -2
  4. package/build/angular/anchor-host-registry.js.map +1 -1
  5. package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
  6. package/build/angular/components/activity-indicator/index.android.js +6 -3
  7. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  8. package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
  9. package/build/angular/components/activity-indicator/index.ios.js +6 -3
  10. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  11. package/build/angular/components/activity-indicator/shared.js +3 -3
  12. package/build/angular/components/button.d.ts +4 -2
  13. package/build/angular/components/button.js +28 -17
  14. package/build/angular/components/button.js.map +1 -1
  15. package/build/angular/components/flat-list/index.d.ts +5 -3
  16. package/build/angular/components/flat-list/index.js +35 -38
  17. package/build/angular/components/flat-list/index.js.map +1 -1
  18. package/build/angular/components/image/index.android.d.ts +8 -3
  19. package/build/angular/components/image/index.android.js +39 -9
  20. package/build/angular/components/image/index.android.js.map +1 -1
  21. package/build/angular/components/image/index.ios.d.ts +8 -3
  22. package/build/angular/components/image/index.ios.js +39 -9
  23. package/build/angular/components/image/index.ios.js.map +1 -1
  24. package/build/angular/components/image/shared.d.ts +4 -1
  25. package/build/angular/components/image/shared.js +33 -2
  26. package/build/angular/components/image/shared.js.map +1 -1
  27. package/build/angular/components/image-background.d.ts +2 -1
  28. package/build/angular/components/image-background.js +6 -4
  29. package/build/angular/components/image-background.js.map +1 -1
  30. package/build/angular/components/input-accessory-view/index.d.ts +9 -4
  31. package/build/angular/components/input-accessory-view/index.js +43 -9
  32. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  33. package/build/angular/components/keyboard-avoiding-view/index.d.ts +2 -1
  34. package/build/angular/components/keyboard-avoiding-view/index.js +6 -4
  35. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  36. package/build/angular/components/modal/index.d.ts +11 -8
  37. package/build/angular/components/modal/index.js +45 -28
  38. package/build/angular/components/modal/index.js.map +1 -1
  39. package/build/angular/components/pressable/index.d.ts +11 -5
  40. package/build/angular/components/pressable/index.js +61 -12
  41. package/build/angular/components/pressable/index.js.map +1 -1
  42. package/build/angular/components/refresh-control/index.d.ts +8 -4
  43. package/build/angular/components/refresh-control/index.js +52 -16
  44. package/build/angular/components/refresh-control/index.js.map +1 -1
  45. package/build/angular/components/safe-area-view/index.d.ts +9 -4
  46. package/build/angular/components/safe-area-view/index.js +46 -9
  47. package/build/angular/components/safe-area-view/index.js.map +1 -1
  48. package/build/angular/components/scroll-view/index.android.d.ts +2 -3
  49. package/build/angular/components/scroll-view/index.android.js +49 -52
  50. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  51. package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
  52. package/build/angular/components/scroll-view/index.ios.js +15 -17
  53. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  54. package/build/angular/components/scroll-view/projection.d.ts +3 -0
  55. package/build/angular/components/scroll-view/projection.js +68 -7
  56. package/build/angular/components/scroll-view/projection.js.map +1 -1
  57. package/build/angular/components/scroll-view/shared.d.ts +14 -5
  58. package/build/angular/components/scroll-view/shared.js +198 -69
  59. package/build/angular/components/scroll-view/shared.js.map +1 -1
  60. package/build/angular/components/scroll-view/sticky-header.js +14 -19
  61. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  62. package/build/angular/components/section-list/index.d.ts +2 -1
  63. package/build/angular/components/section-list/index.js +31 -39
  64. package/build/angular/components/section-list/index.js.map +1 -1
  65. package/build/angular/components/switch/index.android.d.ts +7 -3
  66. package/build/angular/components/switch/index.android.js +38 -10
  67. package/build/angular/components/switch/index.android.js.map +1 -1
  68. package/build/angular/components/switch/index.ios.d.ts +7 -3
  69. package/build/angular/components/switch/index.ios.js +38 -10
  70. package/build/angular/components/switch/index.ios.js.map +1 -1
  71. package/build/angular/components/switch/shared.d.ts +12 -3
  72. package/build/angular/components/switch/shared.js +66 -5
  73. package/build/angular/components/switch/shared.js.map +1 -1
  74. package/build/angular/components/text-input.d.ts +10 -2
  75. package/build/angular/components/text-input.js +46 -6
  76. package/build/angular/components/text-input.js.map +1 -1
  77. package/build/angular/components/touchable/index.d.ts +17 -8
  78. package/build/angular/components/touchable/index.js +78 -19
  79. package/build/angular/components/touchable/index.js.map +1 -1
  80. package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
  81. package/build/angular/components/touchable-native-feedback/index.js +61 -25
  82. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  83. package/build/angular/components/virtualized-list/directives.js +18 -18
  84. package/build/angular/components/virtualized-list/index.d.ts +12 -4
  85. package/build/angular/components/virtualized-list/index.js +163 -68
  86. package/build/angular/components/virtualized-list/index.js.map +1 -1
  87. package/build/angular/components/virtualized-section-list/directives.js +12 -12
  88. package/build/angular/components/virtualized-section-list/index.d.ts +2 -1
  89. package/build/angular/components/virtualized-section-list/index.js +6 -4
  90. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  91. package/build/angular/components.d.ts +1 -1
  92. package/build/angular/components.js +5 -0
  93. package/build/angular/components.js.map +1 -1
  94. package/build/angular/create-portal/index.js +29 -35
  95. package/build/angular/create-portal/index.js.map +1 -1
  96. package/build/angular/create-tunnel/index.js +19 -22
  97. package/build/angular/create-tunnel/index.js.map +1 -1
  98. package/build/angular/descriptor-to-angular/index.js +3 -3
  99. package/build/angular/index.d.ts +4 -2
  100. package/build/angular/index.js +2 -2
  101. package/build/angular/index.js.map +1 -1
  102. package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
  103. package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
  104. package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
  105. package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
  106. package/build/angular/modules/animated/create-animated-component.js +83 -50
  107. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  108. package/build/angular/modules/app-registry/index.js.map +1 -1
  109. package/build/angular/modules/status-bar/index.js +3 -3
  110. package/build/angular/primitives/index.d.ts +1 -1
  111. package/build/angular/primitives/index.js +46 -46
  112. package/build/angular/primitives/index.js.map +1 -1
  113. package/build/angular/primitives/shared.d.ts +39 -1
  114. package/build/angular/primitives/shared.js +110 -9
  115. package/build/angular/primitives/shared.js.map +1 -1
  116. package/build/angular/render/index.js +23 -35
  117. package/build/angular/render/index.js.map +1 -1
  118. package/build/angular/services/color-scheme.service.js +3 -3
  119. package/build/angular/services/window-dimensions.service.js +3 -3
  120. package/package.json +13 -8
  121. package/src/anchor-host-registry.ts +2 -2
  122. package/src/components/activity-indicator/index.android.ts +2 -0
  123. package/src/components/activity-indicator/index.ios.ts +2 -0
  124. package/src/components/button.ts +28 -14
  125. package/src/components/flat-list/index.ts +34 -36
  126. package/src/components/image/index.android.ts +50 -6
  127. package/src/components/image/index.ios.ts +50 -6
  128. package/src/components/image/shared.ts +34 -2
  129. package/src/components/image-background.ts +2 -1
  130. package/src/components/input-accessory-view/index.ts +42 -4
  131. package/src/components/keyboard-avoiding-view/index.ts +7 -1
  132. package/src/components/modal/index.ts +51 -26
  133. package/src/components/pressable/index.ts +67 -8
  134. package/src/components/refresh-control/index.ts +54 -12
  135. package/src/components/safe-area-view/index.ts +50 -5
  136. package/src/components/scroll-view/index.android.ts +34 -41
  137. package/src/components/scroll-view/index.ios.ts +10 -21
  138. package/src/components/scroll-view/projection.ts +79 -7
  139. package/src/components/scroll-view/shared.ts +223 -68
  140. package/src/components/scroll-view/sticky-header.ts +11 -16
  141. package/src/components/section-list/index.ts +27 -36
  142. package/src/components/switch/index.android.ts +48 -7
  143. package/src/components/switch/index.ios.ts +48 -7
  144. package/src/components/switch/shared.ts +79 -3
  145. package/src/components/text-input.ts +52 -2
  146. package/src/components/touchable/index.ts +76 -16
  147. package/src/components/touchable-native-feedback/index.ts +62 -21
  148. package/src/components/virtualized-list/index.ts +137 -44
  149. package/src/components/virtualized-section-list/index.ts +2 -1
  150. package/src/components.ts +5 -0
  151. package/src/create-portal/index.ts +23 -29
  152. package/src/create-tunnel/index.ts +13 -16
  153. package/src/index.ts +30 -1
  154. package/src/modules/animated/animated-leaf-binder.ts +28 -77
  155. package/src/modules/animated/create-animated-component.ts +75 -36
  156. package/src/modules/app-registry/index.ts +6 -8
  157. package/src/primitives/index.ts +1 -0
  158. package/src/primitives/shared.ts +102 -3
  159. package/src/render/index.ts +23 -35
  160. package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
  161. package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
  162. package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
  163. package/src/__tests__/responder-change-detection.test.ts +0 -102
  164. package/src/__tests__/responder-nested-cd.test.ts +0 -113
  165. package/src/angular-gaps.test.ts +0 -142
  166. package/src/angular-imports.test.ts +0 -100
  167. package/src/components/activity-indicator/activity-indicator-class-style.test.ts +0 -44
  168. package/src/components/activity-indicator/activity-indicator.test.ts +0 -131
  169. package/src/components/flat-list/column-wrapper-style-class.test.ts +0 -116
  170. package/src/components/flat-list/flat-list-array-style.test.ts +0 -131
  171. package/src/components/flat-list/flat-list-class-style.test.ts +0 -74
  172. package/src/components/flat-list/flat-list-scroll-containment.test.ts +0 -107
  173. package/src/components/flat-list/flat-list.test.ts +0 -99
  174. package/src/components/image/image-class-style.test.ts +0 -46
  175. package/src/components/image-background-class-style.test.ts +0 -48
  176. package/src/components/image-background-style-class.test.ts +0 -79
  177. package/src/components/input-accessory-view/input-accessory-view.test.ts +0 -43
  178. package/src/components/keyboard-avoiding-view/keyboard-avoiding-view.test.ts +0 -54
  179. package/src/components/modal/modal.test.ts +0 -43
  180. package/src/components/pressable/pressable.test.ts +0 -85
  181. package/src/components/refresh-control/refresh-control.test.ts +0 -41
  182. package/src/components/safe-area-view/safe-area-view.test.ts +0 -43
  183. package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +0 -123
  184. package/src/components/scroll-view/content-container-style-class.test.ts +0 -75
  185. package/src/components/scroll-view/scroll-view-class-style.test.ts +0 -134
  186. package/src/components/scroll-view/scroll-view-projection.test.ts +0 -194
  187. package/src/components/switch/android-switch-class.test.ts +0 -42
  188. package/src/components/switch/switch.test.ts +0 -92
  189. package/src/components/text-input-class-style.test.ts +0 -54
  190. package/src/components/touchable/touchable.test.ts +0 -102
  191. package/src/components/touchable-native-feedback/touchable-native-feedback.test.ts +0 -49
  192. package/src/components/virtualized-list/virtualized-list-class-style.test.ts +0 -76
  193. package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +0 -60
  194. package/src/create-portal/create-portal.test.ts +0 -90
  195. package/src/create-tunnel/create-tunnel.test.ts +0 -120
  196. package/src/descriptor-to-angular/descriptor-outlet.test.ts +0 -94
  197. package/src/host-instance/host-instance.test.ts +0 -49
  198. package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +0 -116
  199. package/src/modules/animated/animated-leaf-binder/index.ts +0 -101
  200. package/src/modules/animated/animated-leaf-binder.test.ts +0 -116
  201. package/src/modules/animated/animated-scroll-view.test.ts +0 -102
  202. package/src/modules/app-registry/app-registry.test.ts +0 -106
  203. package/src/modules/status-bar/status-bar.test.ts +0 -50
  204. package/src/render/render.test.ts +0 -147
  205. package/src/renderer/renderer.test.ts +0 -203
  206. package/src/services/services.test.ts +0 -163
@@ -1,101 +1,52 @@
1
- // Pure Fabrication (GRASP): the leaf-lifecycle orchestration shared by every Animated*
2
- // wrapper, extracted out of AnimatedComponentBase so AnimatedImage which cannot extend
3
- // AnimatedComponentBase, since it must extend ImageBase for TS's-native image handling
4
- // gets the SAME logic via composition instead of copy-pasting it. Framework-thin on
5
- // purpose: it knows nothing about Angular (@ViewChild, DI, change detection), only the
6
- // engine's Animated leaf + commit primitives, so it stays testable without any Angular
7
- // bootstrap. NOTE: react/vue drive this same dance through their own runtime HOC
8
- // (createAnimatedComponent) rather than a class, so there is no cross-adapter dedup
9
- // opportunity here yet this Pure Fabrication is local to the angular adapter.
1
+ // Angular's binding of the shared AnimatedProps leaf lifecycle (core/engine's
2
+ // createAnimatedLeafLifecycle). The POLICY - when to rebuild, when to skip, attach-new-before-
3
+ // detach-old, native event rebinding - lives in the engine and is identical for every adapter;
4
+ // this class supplies only the two things that are genuinely Angular's: how the host node is
5
+ // resolved (a @ViewChild directive, not a ref), and the whenCommitted deferral its batched
6
+ // change detection forces.
7
+ //
8
+ // Pure Fabrication (GRASP): kept as its own class rather than folded into AnimatedComponentBase so
9
+ // AnimatedImage - which must extend ImageBase instead - gets the same wiring by composition rather
10
+ // than copy-paste.
10
11
  import {
11
- AnimatedProps,
12
- attachNativeEventHandler,
13
- dlog,
12
+ createAnimatedLeafLifecycle,
14
13
  whenCommitted,
14
+ type IAnimatedLeafLifecycle,
15
15
  type ISymbioteNode,
16
16
  } from '@symbiote-native/engine';
17
17
 
18
18
  export class AnimatedLeafBinder {
19
- // The leaf currently wired into the value graph. Null until the first reconcile.
20
- private attached: AnimatedProps | null = null;
21
- // The whenCommitted waiter for the current leaf's node binding, cancelled before the next.
22
- private cancelBind: (() => void) | undefined;
23
- // Detachers for any Animated.event prop bound natively to the committed node (JS path: none).
24
- private eventDetachers: Array<() => void> = [];
19
+ private readonly lifecycle: IAnimatedLeafLifecycle;
25
20
 
26
21
  // `resolveNode` stays a caller-supplied resolver (not a constructor-captured node) so this
27
- // class never needs to know HOW the host node is found AnimatedComponentBase and
22
+ // class never needs to know HOW the host node is found - AnimatedComponentBase and
28
23
  // AnimatedImage both resolve it the same way (resolveHostNode + isSymbioteNode over their
29
24
  // own @ViewChild), but that's Angular DI state this binder has no business touching.
30
25
  // `label` is purely diagnostic (dlog), mirroring the constructor.name the pre-extraction
31
26
  // dlog call used.
32
27
  constructor(
33
28
  private readonly resolveNode: () => ISymbioteNode | null,
34
- private readonly label: string,
35
- ) {}
36
-
37
- // Build a fresh AnimatedProps leaf from the current props (the React/Vue per-render
38
- // useMemo(rest)), wire it into the graph, and swap the previous one out. Attach the NEW leaf
39
- // BEFORE detaching the OLD one: a shared Value self-detaches (dropping its native node) the
40
- // instant its child count hits zero, so detaching first would kill a running native animation
41
- // on any unrelated re-render (mirrors RN's AnimatedComponent._attachProps). Then bind the
42
- // committed node, go native if wanted, and rebind native events.
43
- reconcile(props: Record<string, unknown>, wantsNative: boolean): void {
44
- dlog(`${this.label} reconcile`);
45
- const newLeaf = new AnimatedProps(props);
46
- newLeaf.__attach();
47
- if (this.attached !== null && this.attached !== newLeaf) this.attached.__detach();
48
- this.attached = newLeaf;
49
-
50
- this.bindNode(newLeaf, props, wantsNative);
29
+ label: string,
30
+ ) {
31
+ this.lifecycle = createAnimatedLeafLifecycle(label);
51
32
  }
52
33
 
53
- // Bind the leaf to the host's Fabric node THROUGH whenCommitted: under Angular's zoneless
54
- // batched change detection the inner host's tag does not exist yet at ngAfterViewInit time
55
- // (the same async-commit gotcha Vue documents), so binding eagerly would no-op. whenCommitted
56
- // runs the action now if already committed, else after the commit that assigns the tag.
57
- private bindNode(
58
- leaf: AnimatedProps,
59
- props: Record<string, unknown>,
60
- wantsNative: boolean,
61
- ): void {
62
- this.cancelBind?.();
63
- this.cancelBind = undefined;
34
+ // Only the NATIVE half is deferred through whenCommitted (which runs it NOW when the node is
35
+ // already committed - the steady state, and the only path a scroll frame takes). The leaf itself
36
+ // always joins the value graph synchronously: deferring THAT too put the build behind a
37
+ // canceller the next reconcile drops, so a component reconciling faster than it commits never
38
+ // attached anything and a sticky header's rebuilt interpolation never reached the graph.
39
+ // Regression-covered in leaf-attach-before-commit.test.ts.
40
+ reconcile(props: Record<string, unknown>, wantsNative: boolean): void {
64
41
  const node = this.resolveNode();
65
- if (node === null) return;
66
- this.cancelBind = whenCommitted(node, () => {
67
- leaf.setNativeView(node);
68
- if (wantsNative) leaf.__makeNative();
69
- this.attachEvents(node, props);
70
- });
71
- }
72
-
73
- // Native-attach any Animated.event prop (e.g. onScroll={Animated.event(…,{useNativeDriver:true})})
74
- // to the committed node. attachNativeEventHandler no-ops (returns undefined) unless the prop is a
75
- // native event handler with a committed tag, so the JS path stays the fallback. Rebound each
76
- // reconcile so a new inline event re-attaches; detached first to avoid leaking the prior binding.
77
- private attachEvents(node: ISymbioteNode, props: Record<string, unknown>): void {
78
- this.detachEvents();
79
- for (const key of Object.keys(props)) {
80
- const attachment = attachNativeEventHandler(node, key, props[key]);
81
- if (attachment !== undefined) this.eventDetachers.push(attachment.detach);
82
- }
83
- }
84
-
85
- private detachEvents(): void {
86
- for (const detach of this.eventDetachers) detach();
87
- this.eventDetachers = [];
42
+ this.lifecycle.reconcile(props, node, wantsNative, bind =>
43
+ node === null ? undefined : whenCommitted(node, bind),
44
+ );
88
45
  }
89
46
 
90
47
  // Tear down everything: cancel a still-pending bind, drop native event listeners, detach the
91
48
  // current leaf from the value graph. Safe to call even if reconcile() was never called.
92
49
  destroy(): void {
93
- this.cancelBind?.();
94
- this.cancelBind = undefined;
95
- this.detachEvents();
96
- if (this.attached !== null) {
97
- this.attached.__detach();
98
- this.attached = null;
99
- }
50
+ this.lifecycle.teardown();
100
51
  }
101
52
  }
@@ -1,22 +1,18 @@
1
- // createAnimatedComponent for @symbiote-native/angular: the Angular twin of the React/Vue
2
- // wrapper. React/Vue have a runtime HOC (`createAnimatedComponent(Component)` builds a
3
- // fresh wrapped component on the fly). Angular has NO such idiom AND it cannot have one:
4
- // the AOT-under-Metro build compiles every @Component at build
5
- // time and ships NO JIT compiler to Hermes, so a runtime `Component({...})(class)` would
6
- // throw. The idiomatic, AOT-safe equivalent is therefore an EXPLICIT set of standalone
7
- // components AnimatedView / AnimatedText / AnimatedImage / AnimatedScrollView each a
8
- // thin @Component over the matching host primitive, all sharing one abstract lifecycle
9
- // base. The mechanism (build an AnimatedProps leaf, reduce animated props to current
10
- // values for the first paint, bind the leaf to the committed host node, swap leaves on
11
- // re-render, tear down on destroy) lives once in AnimatedComponentBase; only the per-host
12
- // template differs.
1
+ // createAnimatedComponent for @symbiote-native/angular. React/Vue have a runtime HOC
2
+ // (`createAnimatedComponent(Component)` builds a fresh wrapped component on the fly); Angular
3
+ // can't the AOT-under-Metro build compiles every @Component ahead of time and ships no JIT
4
+ // compiler to Hermes, so a runtime `Component({...})(class)` would throw. The AOT-safe
5
+ // equivalent is an EXPLICIT set of standalone components AnimatedView / AnimatedText /
6
+ // AnimatedImage / AnimatedScrollView each a thin @Component over the matching host primitive,
7
+ // sharing one abstract lifecycle base. The mechanism (build an AnimatedProps leaf, reduce
8
+ // animated props to current values for the first paint, bind the leaf to the committed host
9
+ // node, swap leaves on re-render, tear down on destroy) lives once in AnimatedComponentBase.
13
10
  //
14
11
  // The framework-agnostic half (reduceProps / readPassthroughStyle / resolveHostNode /
15
12
  // AnimatedProps / attachNativeEventHandler / isNativeAnimatedAvailable) comes from
16
- // @symbiote-native/engine, shared verbatim with React and Vue. Angular supplies
17
- // only the lifecycle wiring. The per-frame path NEVER goes through Angular change
18
- // detection: value.setValue / animation -> flushValue -> AnimatedProps.update() ->
19
- // setNativeProps(node), exactly as on the other adapters.
13
+ // @symbiote-native/engine, shared with React and Vue. The per-frame path NEVER goes through
14
+ // Angular change detection: value.setValue / animation -> flushValue -> AnimatedProps.update()
15
+ // -> setNativeProps(node), same as the other adapters.
20
16
 
21
17
  import {
22
18
  ChangeDetectionStrategy,
@@ -25,8 +21,10 @@ import {
25
21
  Directive,
26
22
  ElementRef,
27
23
  inject,
24
+ signal,
28
25
  ViewChild,
29
26
  type AfterViewInit,
27
+ type DoCheck,
30
28
  type OnChanges,
31
29
  type OnDestroy,
32
30
  type Type,
@@ -43,11 +41,12 @@ import { selectScrollIntrinsics } from '@symbiote-native/components';
43
41
  import { Image, ScrollView, Text, View } from '../../components';
44
42
  import {
45
43
  anchorHostStyle,
46
- ViewHost,
47
- TextHost,
48
44
  ImageHost,
49
45
  ScrollViewHost,
50
46
  SymbioteHostPropsDirective,
47
+ SymbioteStyleInputDirective,
48
+ TextHost,
49
+ ViewHost,
51
50
  } from '../../primitives';
52
51
  import { FlatList } from '../../components/flat-list';
53
52
  import {
@@ -67,9 +66,8 @@ const PASSTHROUGH_PROP = 'passthroughAnimatedPropExplicitValues';
67
66
  // The inputs every animated wrapper accepts. `style` is the (possibly animated) style — the
68
67
  // common path (TouchableOpacity passes `[style, { opacity }]`). `animatedProps` is the generic
69
68
  // escape hatch for any OTHER prop that may hold an AnimatedNode or an Animated.event handler,
70
- // plus static host props to forward (the React/Vue `rest`). The passthrough drives native.
71
- // Each concrete @Component re-lists these (Angular's inputs-on-base convention, mirroring
72
- // Switch/Image), so they are inherited consistently.
69
+ // plus static host props to forward (the React/Vue `rest`). Each concrete @Component re-lists
70
+ // these (Angular's inputs-on-base convention, mirroring Switch/Image).
73
71
  export const ANIMATED_INPUTS = ['style', 'animatedProps', PASSTHROUGH_PROP];
74
72
  export const ANIMATED_IMAGE_INPUTS = [...IMAGE_INPUTS, 'animatedProps', PASSTHROUGH_PROP];
75
73
 
@@ -79,7 +77,9 @@ export const ANIMATED_IMAGE_INPUTS = [...IMAGE_INPUTS, 'animatedProps', PASSTHRO
79
77
  // @Directive() (no selector) is the Angular-sanctioned decorator for an abstract base that
80
78
  // declares lifecycle hooks — without it ngtsc rejects the inherited hooks (NG2007).
81
79
  @Directive()
82
- export abstract class AnimatedComponentBase implements AfterViewInit, OnChanges, OnDestroy {
80
+ export abstract class AnimatedComponentBase
81
+ implements AfterViewInit, DoCheck, OnChanges, OnDestroy
82
+ {
83
83
  style: unknown;
84
84
  animatedProps: Record<string, unknown> | undefined;
85
85
  passthroughAnimatedPropExplicitValues: unknown;
@@ -115,10 +115,29 @@ export abstract class AnimatedComponentBase implements AfterViewInit, OnChanges,
115
115
  reduced['style'] =
116
116
  reduced['style'] === undefined ? passthroughStyle : [reduced['style'], passthroughStyle];
117
117
  }
118
- reduced['style'] = [anchorHostStyle(this.elementRef), reduced['style']];
118
+ reduced['style'] = [this.anchorStyle(), reduced['style']];
119
119
  return reduced;
120
120
  }
121
121
 
122
+ // The anchor's class-derived style, polled rather than read live inside `reducedProps`.
123
+ //
124
+ // Two different writers land style on this component's anchor and NEITHER goes through
125
+ // ngOnChanges: `class=` / `[class.x]` / `[ngClass]` at the use site (via the renderer's
126
+ // addClass/removeClass -> commitClassStyle), and a `[style]` binding from a composing parent
127
+ // that Angular writes as a host property because `style` is not declared as an @Input here.
128
+ // Reading them live in the getter worked at CREATION only - nothing dirties THIS view when the
129
+ // anchor changes, so the getter was never re-read and the update was lost. Device-visible as a
130
+ // TouchableOpacity whose `class` toggled after mount never repainted.
131
+ //
132
+ // ngDoCheck runs during the PARENT's refresh even when this view is skipped, and the signal
133
+ // write is what then marks this view for refresh. `signal.set`'s own Object.is makes an
134
+ // unchanged poll a no-op, so a steady screen costs one property read per pass and nothing else.
135
+ private readonly anchorStyle = signal<unknown>(undefined);
136
+
137
+ ngDoCheck(): void {
138
+ this.anchorStyle.set(anchorHostStyle(this.elementRef));
139
+ }
140
+
122
141
  ngAfterViewInit(): void {
123
142
  this.viewReady = true;
124
143
  this.reconcile();
@@ -167,6 +186,7 @@ export abstract class AnimatedComponentBase implements AfterViewInit, OnChanges,
167
186
  @Component({
168
187
  selector: 'AnimatedView, symbiote-animated-view',
169
188
  standalone: true,
189
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
170
190
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
171
191
  imports: [SymbioteHostPropsDirective, ViewHost],
172
192
  inputs: ANIMATED_INPUTS,
@@ -182,6 +202,7 @@ export class AnimatedView extends AnimatedComponentBase {}
182
202
  @Component({
183
203
  selector: 'AnimatedText, symbiote-animated-text',
184
204
  standalone: true,
205
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
185
206
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
186
207
  imports: [SymbioteHostPropsDirective, TextHost],
187
208
  inputs: ANIMATED_INPUTS,
@@ -197,6 +218,7 @@ export class AnimatedText extends AnimatedComponentBase {}
197
218
  @Component({
198
219
  selector: 'AnimatedImage, symbiote-animated-image',
199
220
  standalone: true,
221
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
200
222
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
201
223
  imports: [SymbioteHostPropsDirective, ImageHost],
202
224
  inputs: ANIMATED_IMAGE_INPUTS,
@@ -251,6 +273,25 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
251
273
  return resolved;
252
274
  }
253
275
 
276
+ // The anchor's class-derived style, polled rather than read live inside `reducedProps`.
277
+ //
278
+ // Two different writers land style on this component's anchor and NEITHER goes through
279
+ // ngOnChanges: `class=` / `[class.x]` / `[ngClass]` at the use site (via the renderer's
280
+ // addClass/removeClass -> commitClassStyle), and a `[style]` binding from a composing parent
281
+ // that Angular writes as a host property because `style` is not declared as an @Input here.
282
+ // Reading them live in the getter worked at CREATION only - nothing dirties THIS view when the
283
+ // anchor changes, so the getter was never re-read and the update was lost. Device-visible as a
284
+ // TouchableOpacity whose `class` toggled after mount never repainted.
285
+ //
286
+ // ngDoCheck runs during the PARENT's refresh even when this view is skipped, and the signal
287
+ // write is what then marks this view for refresh. `signal.set`'s own Object.is makes an
288
+ // unchanged poll a no-op, so a steady screen costs one property read per pass and nothing else.
289
+ private readonly anchorStyle = signal<unknown>(undefined);
290
+
291
+ ngDoCheck(): void {
292
+ this.anchorStyle.set(anchorHostStyle(this.elementRef));
293
+ }
294
+
254
295
  ngAfterViewInit(): void {
255
296
  this.viewReady = true;
256
297
  this.reconcile();
@@ -291,6 +332,7 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
291
332
  @Component({
292
333
  selector: 'AnimatedScrollView, symbiote-animated-scroll-view',
293
334
  standalone: true,
335
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
294
336
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
295
337
  imports: [SymbioteHostPropsDirective, ScrollViewHost],
296
338
  inputs: ANIMATED_INPUTS,
@@ -305,20 +347,17 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
305
347
  })
306
348
  export class AnimatedScrollView extends AnimatedComponentBase {
307
349
  // RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`) — Android
308
- // needs it explicit or a ScrollView nested inside another scrollable container never
309
- // receives touch (swallowed by the outer scroll view; iOS doesn't gate on this). The real
310
- // ScrollView component already defaults this (scroll-view/shared.ts), but AnimatedScrollView
311
- // talks to the raw primitive directly and has no such defaulting, so it must apply it too.
350
+ // needs it explicit or a nested ScrollView never receives touch (swallowed by the outer one;
351
+ // iOS doesn't gate on this). The real ScrollView component already defaults this
352
+ // (scroll-view/shared.ts), but AnimatedScrollView talks to the raw primitive directly and
353
+ // must apply it too.
312
354
  //
313
- // scrollViewBaseStyle (overflow: 'scroll' + the per-axis flexDirection) is the OTHER thing
314
- // the real ScrollView applies that this bespoke template used to skip entirely — without it,
315
- // iOS Fabric never clips the scroll view's content to its own frame (Android's native
316
- // ViewGroup clips regardless of the style prop, which is why this was invisible there). This
317
- // wrapper has no dedicated `horizontal` input (only the generic `animatedProps`/`style`
318
- // surface), and its template is hardcoded to the vertical intrinsics
319
- // (symbiote-scroll-view/symbiote-scroll-content, not the horizontal variants), so
320
- // selectScrollIntrinsics is always called with isHorizontal=false here — matching what the
321
- // template can actually render.
355
+ // scrollViewBaseStyle (overflow: 'scroll' + the per-axis flexDirection) is the other thing the
356
+ // real ScrollView applies that this bespoke template used to skip — without it, iOS Fabric
357
+ // never clips content to the scroll view's frame (invisible on Android, whose native ViewGroup
358
+ // clips regardless of style). This wrapper has no `horizontal` input and its template is
359
+ // hardcoded to the vertical intrinsics, so selectScrollIntrinsics is always called with
360
+ // isHorizontal=false here, matching what the template can actually render.
322
361
  override get reducedProps(): Record<string, unknown> {
323
362
  const reduced = super.reducedProps;
324
363
  const { scrollViewBaseStyle } = selectScrollIntrinsics(false, undefined);
@@ -15,16 +15,14 @@ import {
15
15
  } from '@symbiote-native/engine';
16
16
  import { mount } from '../../render';
17
17
 
18
- // RN's IComponentProvider: a thunk returning the root component (lazy so the module
19
- // graph stays cheap until the app actually runs).
18
+ // RN's IComponentProvider. Lazy so the module graph stays cheap until the app actually runs.
20
19
  export type IComponentProvider = () => Type<unknown>;
21
20
 
22
- // RN's IWrapperComponentProvider: given the surface's parameters, returns a
23
- // component to wrap the app root in (e.g. a context provider). Optional. Angular has no
24
- // runtime template synthesis (no JIT under AOT/Metrosee create-animated-component.ts),
25
- // so the wrapper must be a pre-authored standalone @Component whose template renders
26
- // `<ng-content>` the Angular idiom for "render my children" (mount() projects the root's
27
- // host node into it, the twin of React's createElement(Wrapper, null, rootElement)).
21
+ // RN's IWrapperComponentProvider (e.g. a context provider). Angular has no runtime template
22
+ // synthesis (no JIT under AOT/Metro see create-animated-component.ts), so the wrapper must be
23
+ // a pre-authored standalone @Component whose template renders `<ng-content>` the Angular idiom
24
+ // for "render my children" (mount() projects the root's host node into it, the twin of React's
25
+ // createElement(Wrapper, null, rootElement)).
28
26
  export type IWrapperComponentProvider = (appParameters: IAppParameters) => Type<unknown>;
29
27
 
30
28
  function runnableFor(
@@ -11,6 +11,7 @@ export {
11
11
  anchorStyleProp,
12
12
  stableAnchorStyle,
13
13
  SymbioteHostPropsDirective,
14
+ SymbioteStyleInputDirective,
14
15
  } from './shared';
15
16
 
16
17
  @Component({
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ChangeDetectorRef,
3
3
  Directive,
4
+ type DoCheck,
4
5
  ElementRef,
5
6
  inject,
6
7
  Input,
@@ -101,6 +102,85 @@ export function stableAnchorStyle(
101
102
 
102
103
  const ON_PREFIX = /^on[A-Z]/;
103
104
 
105
+ // The one `onX` that fires per FRAME rather than per gesture, and with sticky headers RN pins
106
+ // `scrollEventThrottle` to 1 (ScrollView.js), so 60 times a second. Since markForCheck walks
107
+ // RefreshView|Dirty to the ROOT, wrapping it cost one full ancestor-screen template execution per
108
+ // frame - the canary's ~37fps scroll (components/virtualized-list/scroll-cost.test.ts).
109
+ //
110
+ // Nothing lost its refresh: VirtualizedList marks itself when the window moves, sticky rides the
111
+ // native driver, and a caller's own handler is typically an Animated.event touching no Angular
112
+ // state. Drag/momentum begin/end stay wrapped - once per gesture is not a hot path.
113
+ const PER_FRAME_CALLBACK = 'onScroll';
114
+
115
+ /**
116
+ * Restores the OnPush dirty-marking Angular skips for a `[style]` binding that a directive
117
+ * declares as an input. Attach it to any component declaring a `style` input:
118
+ * `hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }]`.
119
+ *
120
+ * An ordinary `[foo]` binding compiles to `ɵɵproperty` -> `setPropertyAndInputs`, whose last act
121
+ * for a component host is `markDirtyIfOnPush` (`render3/instructions/shared.ts`) — the flag
122
+ * `detectChangesInView` needs before it will re-execute a non-CheckAlways view. `[style]` and
123
+ * `[class]` do NOT take that path: they compile to the styling instructions, which on finding a
124
+ * directive input of the same name hand off to `setDirectiveInputsWhichShadowsStyling`
125
+ * (`render3/instructions/property.ts`) — two lines that write the input and stop. No dirty flag.
126
+ * The input lands and `ngOnChanges` fires, but the receiving component's own template, and every
127
+ * getter it reads, is frozen at its creation value. Verified against @angular/core 22.0.8.
128
+ *
129
+ * Symbiote sits squarely on that gap: every composed component declares `style` as a real
130
+ * `@Input()` on purpose, because a raw `[style]` binding would otherwise be decomposed key by key
131
+ * by Angular's CSS style engine, which cannot represent an RN StyleProp array or an Animated value
132
+ * (see `SymbiotePrimitiveHost` above). So the one binding name Angular forgets to mark is the one
133
+ * carrying the whole styling surface.
134
+ *
135
+ * `markForCheck()` here is exactly the skipped `markDirtyIfOnPush`, not a bigger hammer: the write
136
+ * happens inside the parent's refresh, and `markViewDirty` uses the `Dirty` bit alone while views
137
+ * are refreshing (`render3/instructions/mark_view_dirty.ts`), which the ancestors then clear at the
138
+ * end of their own `refreshView`. A component that already bridges its inputs into a signal (the
139
+ * `inputsRevision` + `computed` pattern — Pressable, ScrollView) is immune on its own, since a
140
+ * signal write dirties the template consumer, which `detectChangesInView` honors in any mode.
141
+ */
142
+ @Directive({
143
+ selector: '[symbioteStyleInput]',
144
+ standalone: true,
145
+ inputs: ['style'],
146
+ })
147
+ export class SymbioteStyleInputDirective implements DoCheck, OnChanges {
148
+ private readonly cdr = inject(ChangeDetectorRef);
149
+ // A host directive's ElementRef is the HOST COMPONENT's element - i.e. exactly the anchor a
150
+ // `class=` at the use site resolves onto.
151
+ private readonly elementRef = inject(ElementRef);
152
+ private lastAnchorStyle: unknown;
153
+
154
+ style?: unknown;
155
+
156
+ ngOnChanges(): void {
157
+ this.cdr.markForCheck();
158
+ }
159
+
160
+ // The `class` half of the same gap, and it needs a POLL, not a hook: a class at the use site
161
+ // never becomes an input, so there is no SimpleChanges entry to fire on. Making the view refresh
162
+ // is enough - every composed component re-reads the anchor in a getter on its own.
163
+ //
164
+ // ngDoCheck runs during the PARENT's refresh even when this view would be skipped, so the mark
165
+ // lands in the same pass rather than one behind. The dedup gate is load-bearing: marking on every
166
+ // check re-dirties the view every tick and free-runs CD, the hazard `stableAnchorStyle` above
167
+ // prevents one level up.
168
+ ngDoCheck(): void {
169
+ const current = anchorHostStyle(this.elementRef);
170
+ if (isAnchorStyleUnchanged(this.lastAnchorStyle, current)) return;
171
+ this.lastAnchorStyle = current;
172
+ this.cdr.markForCheck();
173
+ }
174
+ }
175
+
176
+ // Reference equality is the normal path (the engine replaces props.style only on a real class
177
+ // change); the shallow compare guards against an equal-but-fresh object, which would turn the poll
178
+ // above into a CD loop.
179
+ function isAnchorStyleUnchanged(previous: unknown, next: unknown): boolean {
180
+ if (Object.is(previous, next)) return true;
181
+ return isRecord(previous) && isRecord(next) && shallowStyleEqual(previous, next);
182
+ }
183
+
104
184
  /**
105
185
  * Base for all Angular primitive host components (`symbiote-view`, `symbiote-text`, ...).
106
186
  * RN's `StyleProp` can be an object, an array, nested arrays, falsy entries, and Animated
@@ -156,6 +236,17 @@ export class SymbioteHostPropsDirective {
156
236
  // The directive lives in its HOST component's template, so its injected ChangeDetectorRef IS
157
237
  // that component's own view detector — the one thing that can refresh it (see wrapCallback).
158
238
  private readonly cdr = inject(ChangeDetectorRef);
239
+ // One wrapper per ORIGINAL handler, for the lifetime of this directive. Without it every push
240
+ // of the props bag allocated a brand-new closure for every `onX` key, and the engine stored it
241
+ // as a new listener (`setEventListener` allocates its own closure on top) - per key, per push,
242
+ // on a path a scroll frame reaches 60 times a second. The wrapper body closes over nothing but
243
+ // `value` and this instance's `cdr`, so the handler alone is a complete cache key; `key` is
244
+ // read only by the ON_PREFIX guard above and never enters the wrapper.
245
+ //
246
+ // Reference stability is the point, not just the allocation: a fresh function on every push
247
+ // defeats every downstream identity check on the bag, so nothing upstream could ever conclude
248
+ // "this bag is unchanged" while a callback prop was in it.
249
+ private readonly wrappers = new WeakMap<object, (...args: unknown[]) => unknown>();
159
250
 
160
251
  get node(): unknown {
161
252
  return this.elementRef.nativeElement;
@@ -180,11 +271,19 @@ export class SymbioteHostPropsDirective {
180
271
  // for free (setState / proxy reactivity).
181
272
  private wrapCallback(key: string, value: unknown): unknown {
182
273
  if (!ON_PREFIX.test(key) || typeof value !== 'function') return value;
183
- const handler = value as (...args: unknown[]) => unknown;
184
- return (...args: unknown[]): unknown => {
185
- const result = handler(...args);
274
+ if (key === PER_FRAME_CALLBACK) return value;
275
+ const cached = this.wrappers.get(value);
276
+ if (cached !== undefined) return cached;
277
+ // Reflect.apply rather than calling a cast-to-signature local: `typeof value === 'function'`
278
+ // narrows to `Function`, which has no call signature TypeScript will accept, and this repo
279
+ // does not use `as` to paper over that. `undefined` as the receiver preserves the previous
280
+ // unbound call.
281
+ const wrapper = (...args: unknown[]): unknown => {
282
+ const result: unknown = Reflect.apply(value, undefined, args);
186
283
  this.cdr.markForCheck();
187
284
  return result;
188
285
  };
286
+ this.wrappers.set(value, wrapper);
287
+ return wrapper;
189
288
  }
190
289
  }
@@ -119,43 +119,31 @@ export function mount(
119
119
  useValue: new SymbioteRendererFactory(surface),
120
120
  },
121
121
  { provide: DOCUMENT, useValue: { head: surface, body: surface } },
122
- // createEnvironmentInjector with a null parent gives this injector scope
123
- // {'environment'} only (see EnvironmentNgModuleRefAdapter), so providedIn:'root'
124
- // tokens — ApplicationRef included — never resolve on their own (R3Injector.get
125
- // walks up looking for an injector whose `scopes` contains 'root', and a null
126
- // parent means that search always dead-ends in NullInjector). platform-browser's
127
- // BROWSER_MODULE_PROVIDERS solves this the same way for real DOM apps: it hands
128
- // { provide: INJECTOR_SCOPE, useValue: 'root' } to its OWN app-level providers, and
129
- // R3Injector's constructor reads that provider and self-tags this.scopes with
130
- // 'root'. We do the same here — no PlatformRef, no DOM, just this one provider.
122
+ // createEnvironmentInjector with a null parent scopes this injector to {'environment'}
123
+ // only (see EnvironmentNgModuleRefAdapter), so providedIn:'root' tokens — ApplicationRef
124
+ // included — never resolve (R3Injector.get walks up for a `scopes` containing 'root', and
125
+ // a null parent always dead-ends in NullInjector). platform-browser solves this the same
126
+ // way for real DOM apps via BROWSER_MODULE_PROVIDERS: hand { provide: INJECTOR_SCOPE,
127
+ // useValue: 'root' } to the app-level providers, which R3Injector's constructor reads to
128
+ // self-tag this.scopes with 'root'. Same trick here, no PlatformRef, no DOM.
131
129
  { provide: INJECTOR_SCOPE, useValue: 'root' },
132
- // Supplies the real ChangeDetectionSchedulerImpl (microtask-batched, self-scheduling
133
- // via ApplicationRef.afterTick) + NoopNgZone + ZONELESS_ENABLED: true — the exact
134
- // bundle internalCreateApplication() uses. With ApplicationRef reachable (see above),
135
- // there is no more reason for a hand-rolled scheduler: it replaces our old
136
- // unconditional `rootView.detectChanges(); cmpView.detectChanges()` (which force-ran
137
- // BOTH root views on every tick regardless of cause) with Angular's own tick(), which
138
- // only enters an attached view when something actually marked it (RefreshView / Dirty
139
- // consumer / HasChildViewsToRefresh). NOTE: this does NOT stop the root's own template
140
- // from re-running on a plain press or `ChangeDetectorRef.markForCheck()` anywhere in
141
- // the tree `markViewDirty` (which both native (event) bindings and `markForCheck()`
142
- // go through, see SymbioteHostPropsDirective) unconditionally sets RefreshView on
143
- // EVERY ancestor up to the root; that is fundamental, unavoidable Angular zoneless
144
- // behavior, true in every Angular app, not something this swap changes. What DOES
145
- // still protect a sibling branch from an unrelated press is a genuine child
146
- // `@Component` boundary — SignalView-compiled children are skip-eligible regardless of
147
- // this scheduler.
130
+ // Supplies the real ChangeDetectionSchedulerImpl (microtask-batched via
131
+ // ApplicationRef.afterTick) + NoopNgZone + ZONELESS_ENABLED: true — the exact bundle
132
+ // internalCreateApplication() uses. Replaces the old unconditional
133
+ // `rootView.detectChanges(); cmpView.detectChanges()` (force-ran both root views on every
134
+ // tick) with Angular's own tick(), which only enters a view something actually marked
135
+ // dirty. This does NOT stop the root's own template from re-running on a plain press or
136
+ // `markForCheck()` anywhere in the tree `markViewDirty` unconditionally sets RefreshView
137
+ // on every ancestor up to the root; that's fundamental Angular zoneless behavior, not
138
+ // something this swap changes. A genuine child `@Component` boundary still protects a
139
+ // sibling branch from an unrelated press.
148
140
  ...provideZonelessChangeDetectionInternal(),
149
- // Angular's own INTERNAL_APPLICATION_ERROR_HANDLER (core.mjs) reports a tick()
150
- // exception by calling `injector.get(ErrorHandler)` a normal `bootstrapApplication`
151
- // registers this token by default (platform-browser's BROWSER_MODULE_PROVIDERS), but
152
- // our from-scratch environment injector never did, so that lookup itself threw
153
- // NG0201 and REPLACED whatever the real error was with an unrelated "No provider
154
- // found for ErrorHandler" the real exception never got logged, and the NG0201 itself
155
- // propagated out of a bare Timeout callback uncaught (nothing above it in the stack to
156
- // catch it), i.e. any async tick() exception, anywhere in the app, crashed hard instead
157
- // of being reported. Providing the default `ErrorHandler` (same one bootstrapApplication
158
- // ships) restores the intended behavior: `console.error('ERROR', e)` and keep running.
141
+ // Angular's INTERNAL_APPLICATION_ERROR_HANDLER reports a tick() exception via
142
+ // `injector.get(ErrorHandler)`; a normal `bootstrapApplication` registers that token by
143
+ // default, but our from-scratch environment injector never did, so the lookup itself threw
144
+ // 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.
159
147
  { provide: ErrorHandler, useClass: ErrorHandler },
160
148
  ColorSchemeService,
161
149
  WindowDimensionsService,
@@ -1,14 +0,0 @@
1
- import { type ISymbioteNode } from '@symbiote-native/engine';
2
- export declare class AnimatedLeafBinder {
3
- private readonly resolveNode;
4
- private readonly label;
5
- private attached;
6
- private cancelBind;
7
- private eventDetachers;
8
- constructor(resolveNode: () => ISymbioteNode | null, label: string);
9
- reconcile(props: Record<string, unknown>, wantsNative: boolean): void;
10
- private bindNode;
11
- private attachEvents;
12
- private detachEvents;
13
- destroy(): void;
14
- }