@symbiote-native/angular 0.6.2 → 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 (162) hide show
  1. package/bin/symbiote-angular-dev.cjs +20 -19
  2. package/build/angular/anchor-host-registry.js +2 -2
  3. package/build/angular/anchor-host-registry.js.map +1 -1
  4. package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
  5. package/build/angular/components/activity-indicator/index.android.js +6 -3
  6. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  7. package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
  8. package/build/angular/components/activity-indicator/index.ios.js +6 -3
  9. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  10. package/build/angular/components/activity-indicator/shared.js +3 -3
  11. package/build/angular/components/button.d.ts +4 -2
  12. package/build/angular/components/button.js +28 -17
  13. package/build/angular/components/button.js.map +1 -1
  14. package/build/angular/components/flat-list/index.d.ts +5 -3
  15. package/build/angular/components/flat-list/index.js +35 -38
  16. package/build/angular/components/flat-list/index.js.map +1 -1
  17. package/build/angular/components/image/index.android.d.ts +8 -3
  18. package/build/angular/components/image/index.android.js +39 -9
  19. package/build/angular/components/image/index.android.js.map +1 -1
  20. package/build/angular/components/image/index.ios.d.ts +8 -3
  21. package/build/angular/components/image/index.ios.js +39 -9
  22. package/build/angular/components/image/index.ios.js.map +1 -1
  23. package/build/angular/components/image/shared.d.ts +4 -1
  24. package/build/angular/components/image/shared.js +33 -2
  25. package/build/angular/components/image/shared.js.map +1 -1
  26. package/build/angular/components/image-background.d.ts +2 -1
  27. package/build/angular/components/image-background.js +6 -4
  28. package/build/angular/components/image-background.js.map +1 -1
  29. package/build/angular/components/input-accessory-view/index.d.ts +9 -4
  30. package/build/angular/components/input-accessory-view/index.js +43 -9
  31. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  32. package/build/angular/components/keyboard-avoiding-view/index.d.ts +2 -1
  33. package/build/angular/components/keyboard-avoiding-view/index.js +6 -4
  34. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  35. package/build/angular/components/modal/index.d.ts +11 -8
  36. package/build/angular/components/modal/index.js +45 -28
  37. package/build/angular/components/modal/index.js.map +1 -1
  38. package/build/angular/components/pressable/index.d.ts +11 -5
  39. package/build/angular/components/pressable/index.js +61 -12
  40. package/build/angular/components/pressable/index.js.map +1 -1
  41. package/build/angular/components/refresh-control/index.d.ts +8 -4
  42. package/build/angular/components/refresh-control/index.js +52 -16
  43. package/build/angular/components/refresh-control/index.js.map +1 -1
  44. package/build/angular/components/safe-area-view/index.d.ts +9 -4
  45. package/build/angular/components/safe-area-view/index.js +46 -9
  46. package/build/angular/components/safe-area-view/index.js.map +1 -1
  47. package/build/angular/components/scroll-view/index.android.d.ts +2 -3
  48. package/build/angular/components/scroll-view/index.android.js +49 -52
  49. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  50. package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
  51. package/build/angular/components/scroll-view/index.ios.js +15 -17
  52. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  53. package/build/angular/components/scroll-view/projection.d.ts +3 -0
  54. package/build/angular/components/scroll-view/projection.js +68 -7
  55. package/build/angular/components/scroll-view/projection.js.map +1 -1
  56. package/build/angular/components/scroll-view/shared.d.ts +14 -5
  57. package/build/angular/components/scroll-view/shared.js +198 -69
  58. package/build/angular/components/scroll-view/shared.js.map +1 -1
  59. package/build/angular/components/scroll-view/sticky-header.js +14 -19
  60. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  61. package/build/angular/components/section-list/index.d.ts +2 -1
  62. package/build/angular/components/section-list/index.js +31 -39
  63. package/build/angular/components/section-list/index.js.map +1 -1
  64. package/build/angular/components/switch/index.android.d.ts +7 -3
  65. package/build/angular/components/switch/index.android.js +38 -10
  66. package/build/angular/components/switch/index.android.js.map +1 -1
  67. package/build/angular/components/switch/index.ios.d.ts +7 -3
  68. package/build/angular/components/switch/index.ios.js +38 -10
  69. package/build/angular/components/switch/index.ios.js.map +1 -1
  70. package/build/angular/components/switch/shared.d.ts +12 -3
  71. package/build/angular/components/switch/shared.js +66 -5
  72. package/build/angular/components/switch/shared.js.map +1 -1
  73. package/build/angular/components/text-input.d.ts +10 -2
  74. package/build/angular/components/text-input.js +46 -6
  75. package/build/angular/components/text-input.js.map +1 -1
  76. package/build/angular/components/touchable/index.d.ts +17 -8
  77. package/build/angular/components/touchable/index.js +78 -19
  78. package/build/angular/components/touchable/index.js.map +1 -1
  79. package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
  80. package/build/angular/components/touchable-native-feedback/index.js +61 -25
  81. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  82. package/build/angular/components/virtualized-list/directives.js +18 -18
  83. package/build/angular/components/virtualized-list/index.d.ts +12 -4
  84. package/build/angular/components/virtualized-list/index.js +163 -68
  85. package/build/angular/components/virtualized-list/index.js.map +1 -1
  86. package/build/angular/components/virtualized-section-list/directives.js +12 -12
  87. package/build/angular/components/virtualized-section-list/index.d.ts +2 -1
  88. package/build/angular/components/virtualized-section-list/index.js +6 -4
  89. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  90. package/build/angular/components.d.ts +1 -1
  91. package/build/angular/components.js +5 -0
  92. package/build/angular/components.js.map +1 -1
  93. package/build/angular/create-portal/index.js +29 -35
  94. package/build/angular/create-portal/index.js.map +1 -1
  95. package/build/angular/create-tunnel/index.js +19 -22
  96. package/build/angular/create-tunnel/index.js.map +1 -1
  97. package/build/angular/descriptor-to-angular/index.js +3 -3
  98. package/build/angular/index.d.ts +4 -2
  99. package/build/angular/index.js +2 -2
  100. package/build/angular/index.js.map +1 -1
  101. package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
  102. package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
  103. package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
  104. package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
  105. package/build/angular/modules/animated/create-animated-component.js +83 -50
  106. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  107. package/build/angular/modules/app-registry/index.js.map +1 -1
  108. package/build/angular/modules/status-bar/index.js +3 -3
  109. package/build/angular/primitives/index.d.ts +1 -1
  110. package/build/angular/primitives/index.js +46 -46
  111. package/build/angular/primitives/index.js.map +1 -1
  112. package/build/angular/primitives/shared.d.ts +39 -1
  113. package/build/angular/primitives/shared.js +110 -9
  114. package/build/angular/primitives/shared.js.map +1 -1
  115. package/build/angular/render/index.js +23 -35
  116. package/build/angular/render/index.js.map +1 -1
  117. package/build/angular/services/color-scheme.service.js +3 -3
  118. package/build/angular/services/window-dimensions.service.js +3 -3
  119. package/package.json +9 -7
  120. package/src/anchor-host-registry.ts +2 -2
  121. package/src/components/activity-indicator/index.android.ts +2 -0
  122. package/src/components/activity-indicator/index.ios.ts +2 -0
  123. package/src/components/button.ts +28 -14
  124. package/src/components/flat-list/index.ts +34 -36
  125. package/src/components/image/index.android.ts +50 -6
  126. package/src/components/image/index.ios.ts +50 -6
  127. package/src/components/image/shared.ts +34 -2
  128. package/src/components/image-background.ts +2 -1
  129. package/src/components/input-accessory-view/index.ts +42 -4
  130. package/src/components/keyboard-avoiding-view/index.ts +7 -1
  131. package/src/components/modal/index.ts +51 -26
  132. package/src/components/pressable/index.ts +67 -8
  133. package/src/components/refresh-control/index.ts +54 -12
  134. package/src/components/safe-area-view/index.ts +50 -5
  135. package/src/components/scroll-view/index.android.ts +34 -41
  136. package/src/components/scroll-view/index.ios.ts +10 -21
  137. package/src/components/scroll-view/projection.ts +79 -7
  138. package/src/components/scroll-view/shared.ts +223 -68
  139. package/src/components/scroll-view/sticky-header.ts +11 -16
  140. package/src/components/section-list/index.ts +27 -36
  141. package/src/components/switch/index.android.ts +48 -7
  142. package/src/components/switch/index.ios.ts +48 -7
  143. package/src/components/switch/shared.ts +79 -3
  144. package/src/components/text-input.ts +52 -2
  145. package/src/components/touchable/index.ts +76 -16
  146. package/src/components/touchable-native-feedback/index.ts +62 -21
  147. package/src/components/virtualized-list/index.ts +137 -44
  148. package/src/components/virtualized-section-list/index.ts +2 -1
  149. package/src/components.ts +5 -0
  150. package/src/create-portal/index.ts +23 -29
  151. package/src/create-tunnel/index.ts +13 -16
  152. package/src/index.ts +30 -1
  153. package/src/modules/animated/animated-leaf-binder.ts +28 -77
  154. package/src/modules/animated/create-animated-component.ts +75 -36
  155. package/src/modules/app-registry/index.ts +6 -8
  156. package/src/primitives/index.ts +1 -0
  157. package/src/primitives/shared.ts +102 -3
  158. package/src/render/index.ts +23 -35
  159. package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
  160. package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
  161. package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
  162. package/src/modules/animated/animated-leaf-binder/index.ts +0 -101
@@ -1,23 +1,22 @@
1
- // ScrollView on Android. An Android ScrollView accepts only ONE child, so a
2
- // RefreshControl can't be a sibling of the content the way iOS allows ("addViewAt: failed to insert
3
- // view ... at index 1"). RN solves this by having the RefreshControl (AndroidSwipeRefreshLayout)
4
- // WRAP the scroll view (cloneElement(refreshControl, {style}, scrollView)). Angular mirrors that
5
- // shape by querying a projected <RefreshControl>, re-rendering its native prop surface as the outer
6
- // wrapper, and projecting every non-RefreshControl child into the inner scroll content.
1
+ // ScrollView on Android. An Android ScrollView accepts only ONE child, so a RefreshControl can't
2
+ // be a sibling of the content the way iOS allows ("addViewAt: failed to insert view ... at index
3
+ // 1"). RN solves this by having the RefreshControl (AndroidSwipeRefreshLayout) WRAP the scroll
4
+ // view (cloneElement(refreshControl, {style}, scrollView)). Angular mirrors that shape by querying
5
+ // a projected <RefreshControl>, re-rendering its native prop surface as the outer wrapper, and
6
+ // projecting every non-RefreshControl child into the inner scroll content.
7
7
  //
8
- // Android genuinely needs a different Fabric view per
9
- // axis for BOTH the outer scroll container (RCTScrollView is vertical-only; AndroidHorizontalScrollView
10
- // is a dedicated ViewManager) AND the inner content view (vertical content is a plain Android View,
11
- // horizontal content is AndroidHorizontalScrollContentView, which carries its own ShadowNode::
12
- // layout() override). So the content tag differs by axis but Angular only reliably projects into a
13
- // component's LAST-declared <ng-content> when 2+ DISTINCT declarations exist (angular/angular#22972),
14
- // which an earlier 4-call-site template (one <ng-content> per axis x refresh-control branch) tripped.
15
- // Fix: exactly ONE `<ng-content>` textually, inside a single top-level `<ng-template>`, re-instantiated
16
- // into whichever branch is active via a local template-outlet directive (SymbioteTemplateOutletDirective
17
- // below, `@angular/common`-free per this adapter's dependency policy — see package.json). Only one
18
- // branch is ever live at a time, and the `<ng-content>` DECLARATION COUNT stays at one, so the "last
19
- // one wins" limitation never triggers. Covered by `android-scroll-view-axis-projection.test.ts` (all
20
- // four axis x refresh combos, plus a runtime axis switch) and `flat-list-scroll-containment.test.ts`.
8
+ // Android needs a different Fabric view per axis for both the outer scroll container
9
+ // (RCTScrollView is vertical-only; AndroidHorizontalScrollView is a dedicated ViewManager) and the
10
+ // inner content view (horizontal content is AndroidHorizontalScrollContentView, with its own
11
+ // ShadowNode::layout() override) but Angular only reliably projects into a component's
12
+ // LAST-declared <ng-content> when 2+ DISTINCT declarations exist (angular/angular#22972), which an
13
+ // earlier 4-call-site template (one <ng-content> per axis x refresh-control branch) tripped. Fix:
14
+ // exactly ONE `<ng-content>` textually, inside a single top-level `<ng-template>`, re-instantiated
15
+ // into whichever branch is active via a local template-outlet directive
16
+ // (SymbioteTemplateOutletDirective below, `@angular/common`-free per this adapter's dependency
17
+ // policy — see package.json). Only one branch is ever live, so the declaration count stays at one
18
+ // and the "last one wins" limitation never triggers. Covered by
19
+ // `android-scroll-view-axis-projection.test.ts` and `flat-list-scroll-containment.test.ts`.
21
20
  //
22
21
  // Metro picks this on an Android host; no Platform.OS read.
23
22
 
@@ -26,7 +25,6 @@ import {
26
25
  ChangeDetectionStrategy,
27
26
  Component,
28
27
  Directive,
29
- ElementRef,
30
28
  Input,
31
29
  TemplateRef,
32
30
  ViewContainerRef,
@@ -37,13 +35,13 @@ import {
37
35
  } from '@angular/core';
38
36
  import type { IStyleProp, IViewStyle } from '@symbiote-native/engine';
39
37
  import {
40
- anchorHostStyle,
41
- HorizontalScrollView,
42
38
  HorizontalScrollContentView,
39
+ HorizontalScrollView,
43
40
  RefreshControlHost,
44
41
  ScrollContentView,
45
42
  ScrollViewHost,
46
43
  SymbioteHostPropsDirective,
44
+ SymbioteStyleInputDirective,
47
45
  } from '../../primitives';
48
46
  import { ScrollViewBase, ScrollViewProjectionDirective, SCROLL_VIEW_INPUTS } from './shared';
49
47
  export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
@@ -89,6 +87,7 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
89
87
  @Component({
90
88
  selector: 'ScrollView',
91
89
  standalone: true,
90
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
92
91
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
93
92
  imports: [
94
93
  ScrollViewHost,
@@ -115,10 +114,10 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
115
114
  >
116
115
  <symbiote-horizontal-scroll-view
117
116
  #host="symbioteHost"
118
- [symbioteHostProps]="androidWrappedScrollProps"
117
+ [symbioteHostProps]="androidWrappedScrollProps()"
119
118
  >
120
119
  <symbiote-horizontal-scroll-content
121
- [symbioteHostProps]="contentProps"
120
+ [symbioteHostProps]="contentProps()"
122
121
  [symbioteScrollViewProjection]="projectionController"
123
122
  >
124
123
  <ng-container [symbioteTemplateOutlet]="sharedContent"></ng-container>
@@ -126,9 +125,9 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
126
125
  </symbiote-horizontal-scroll-view>
127
126
  </symbiote-refresh-control>
128
127
  } @else {
129
- <symbiote-horizontal-scroll-view #host="symbioteHost" [symbioteHostProps]="scrollProps">
128
+ <symbiote-horizontal-scroll-view #host="symbioteHost" [symbioteHostProps]="scrollProps()">
130
129
  <symbiote-horizontal-scroll-content
131
- [symbioteHostProps]="contentProps"
130
+ [symbioteHostProps]="contentProps()"
132
131
  [symbioteScrollViewProjection]="projectionController"
133
132
  >
134
133
  <ng-container [symbioteTemplateOutlet]="sharedContent"></ng-container>
@@ -144,10 +143,10 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
144
143
  >
145
144
  <symbiote-scroll-view
146
145
  #host="symbioteHost"
147
- [symbioteHostProps]="androidWrappedScrollProps"
146
+ [symbioteHostProps]="androidWrappedScrollProps()"
148
147
  >
149
148
  <symbiote-scroll-content
150
- [symbioteHostProps]="contentProps"
149
+ [symbioteHostProps]="contentProps()"
151
150
  [symbioteScrollViewProjection]="projectionController"
152
151
  >
153
152
  <ng-container [symbioteTemplateOutlet]="sharedContent"></ng-container>
@@ -155,9 +154,9 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
155
154
  </symbiote-scroll-view>
156
155
  </symbiote-refresh-control>
157
156
  } @else {
158
- <symbiote-scroll-view #host="symbioteHost" [symbioteHostProps]="scrollProps">
157
+ <symbiote-scroll-view #host="symbioteHost" [symbioteHostProps]="scrollProps()">
159
158
  <symbiote-scroll-content
160
- [symbioteHostProps]="contentProps"
159
+ [symbioteHostProps]="contentProps()"
161
160
  [symbioteScrollViewProjection]="projectionController"
162
161
  >
163
162
  <ng-container [symbioteTemplateOutlet]="sharedContent"></ng-container>
@@ -168,23 +167,17 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
168
167
  `,
169
168
  })
170
169
  export class ScrollView extends ScrollViewBase {
171
- // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
172
- // onto (see anchorHostStyle's doc comment) — NOT `#host` in the template above, which targets
173
- // the real inner `symbiote-scroll-view`/`symbiote-horizontal-scroll-view` one level down.
174
- private readonly elementRef = inject(ElementRef);
175
-
176
- override get scrollProps(): Record<string, unknown> {
177
- const props = super.scrollProps;
178
- return { ...props, style: [anchorHostStyle(this.elementRef), props.style] };
179
- }
180
-
181
170
  // Feeds the anchor's class-derived style INTO the base class's own `splitLayoutProps` call
182
171
  // (shared.ts's `androidWrappedScrollProps`/`androidRefreshControlProps`), not tacked on after —
183
172
  // a class-only layout style (flex/height/gap/…) must be visible to the split itself, or the
184
173
  // Android outer refresh-control wrapper never receives its share and collapses to zero size
185
174
  // (a real device bug: the whole ScrollView renders nothing). See layoutSplitStyle's doc comment
186
175
  // in shared.ts and the identical fix already shipped in the Vue adapter's scroll-view.
176
+ //
177
+ // Reads the base's `anchorStyle` SIGNAL rather than calling anchorHostStyle() directly: this
178
+ // getter is evaluated inside the `androidWrappedScrollProps` computed, where a plain read would
179
+ // be untracked and leave the memoized bag holding a stale class.
187
180
  protected override get layoutSplitStyle(): IStyleProp<IViewStyle> {
188
- return [asStyle(anchorHostStyle(this.elementRef)), this.style];
181
+ return [asStyle(this.anchorStyle()), this.style];
189
182
  }
190
183
  }
@@ -16,19 +16,13 @@
16
16
  // a horizontal FlatList's cells landed outside the ScrollView entirely. Fix: single unconditional
17
17
  // `<ng-content>` call site.
18
18
 
19
+ import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, Component } from '@angular/core';
19
20
  import {
20
- CUSTOM_ELEMENTS_SCHEMA,
21
- ChangeDetectionStrategy,
22
- Component,
23
- ElementRef,
24
- inject,
25
- } from '@angular/core';
26
- import {
27
- anchorHostStyle,
28
21
  RefreshControlHost,
29
22
  ScrollContentView,
30
23
  ScrollViewHost,
31
24
  SymbioteHostPropsDirective,
25
+ SymbioteStyleInputDirective,
32
26
  } from '../../primitives';
33
27
  import { ScrollViewBase, ScrollViewProjectionDirective, SCROLL_VIEW_INPUTS } from './shared';
34
28
  export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
@@ -38,6 +32,7 @@ export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
38
32
  @Component({
39
33
  selector: 'ScrollView',
40
34
  standalone: true,
35
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
41
36
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
42
37
  imports: [
43
38
  ScrollViewHost,
@@ -49,7 +44,7 @@ export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
49
44
  changeDetection: ChangeDetectionStrategy.OnPush,
50
45
  inputs: SCROLL_VIEW_INPUTS,
51
46
  template: `
52
- <symbiote-scroll-view #host="symbioteHost" [symbioteHostProps]="scrollProps">
47
+ <symbiote-scroll-view #host="symbioteHost" [symbioteHostProps]="scrollProps()">
53
48
  @if (hasProjectedRefreshControl) {
54
49
  <symbiote-refresh-control
55
50
  #refreshHost="symbioteHost"
@@ -58,7 +53,7 @@ export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
58
53
  ></symbiote-refresh-control>
59
54
  }
60
55
  <symbiote-scroll-content
61
- [symbioteHostProps]="contentProps"
56
+ [symbioteHostProps]="contentProps()"
62
57
  [symbioteScrollViewProjection]="projectionController"
63
58
  >
64
59
  <ng-content></ng-content>
@@ -66,14 +61,8 @@ export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
66
61
  </symbiote-scroll-view>
67
62
  `,
68
63
  })
69
- export class ScrollView extends ScrollViewBase {
70
- // This component's OWN host the non-painting anchor `class="..."` at the use site resolves
71
- // onto (see anchorHostStyle's doc comment) NOT `#host` in the template above, which targets
72
- // the real inner `symbiote-scroll-view` one level down.
73
- private readonly elementRef = inject(ElementRef);
74
-
75
- override get scrollProps(): Record<string, unknown> {
76
- const props = super.scrollProps;
77
- return { ...props, style: [anchorHostStyle(this.elementRef), props.style] };
78
- }
79
- }
64
+ // Behavior lives entirely in ScrollViewBase; this class exists for the decorator + iOS template.
65
+ // The anchor-style merge that used to be a `scrollProps` override here moved into the base's
66
+ // `scrollProps` computed - a subclass getter cannot override a field, and both platforms merged it
67
+ // identically anyway (see that computed's comment).
68
+ export class ScrollView extends ScrollViewBase {}
@@ -73,6 +73,9 @@ function isProjectedRefreshControl(node: ISymbioteNode): boolean {
73
73
  // delay, rebuild ranges, cross-talk record) live there; this runner only EXECUTES the effects on the
74
74
  // engine node (build interpolation + wire listener, hold the debounce timer, re-apply the node props).
75
75
  // Because it owns the child index, it consumes the reducer's record-header-y effect for cross-talk.
76
+ // Diagnostic-only: a process-wide counter so each controller's log lines are attributable.
77
+ let projectionControllerSeq = 0;
78
+
76
79
  class StickyProjectionWrapper {
77
80
  private readonly state = createInitialStickyState();
78
81
  private animatedTranslateY: AnimatedInterpolation;
@@ -91,6 +94,9 @@ class StickyProjectionWrapper {
91
94
  inputRange: [-1, 0],
92
95
  outputRange: [0, 0],
93
96
  });
97
+ dlog(
98
+ `STICKY[wrap] created index=${childIndex} scrollValueIsNative=${this.controller.config.scrollAnimatedValue.__isNative()}`,
99
+ );
94
100
  this.dispatch({ kind: 'inputs-changed' });
95
101
  }
96
102
 
@@ -148,7 +154,7 @@ class StickyProjectionWrapper {
148
154
  this.animatedTranslateY = next;
149
155
  this.applyProps();
150
156
  dlog(
151
- `Angular ScrollView projection sticky index=${this.childIndex} measured=${this.state.measured} y=${this.state.layoutY}`,
157
+ `STICKY[wrap] rebuilt index=${this.childIndex} measured=${this.state.measured} y=${this.state.layoutY}`,
152
158
  );
153
159
  break;
154
160
  }
@@ -172,6 +178,7 @@ class StickyProjectionWrapper {
172
178
  private readonly onLayout = (event: ISymbioteEvent): void => {
173
179
  const y = readLayoutNumber(event, 'y');
174
180
  const height = readLayoutNumber(event, 'height');
181
+ dlog(`STICKY[wrap] index=${this.childIndex} onLayout y=${y} height=${height}`);
175
182
  // Keep the previous value when a field is absent (RN sets state only on a defined read).
176
183
  this.dispatch({
177
184
  kind: 'layout',
@@ -180,7 +187,12 @@ class StickyProjectionWrapper {
180
187
  });
181
188
  };
182
189
 
190
+ // The ONLY thing that drives the committed pin on this path: applyProps has no __makeNative(),
191
+ // so the wrapper is JS-driven end to end. If the scroll value has been made native up front the
192
+ // cascade to child listeners stops (AnimatedWithChildren.__callListeners skips children once
193
+ // isNative) and this never fires - the header then renders in place and never moves.
183
194
  private readonly animatedValueListener = ({ value }: { value: number | string }): void => {
195
+ dlog(() => `STICKY[wrap] index=${this.childIndex} animated-tick value=${String(value)}`);
184
196
  if (typeof value === 'number') this.dispatch({ kind: 'animated-tick', value });
185
197
  };
186
198
 
@@ -203,14 +215,37 @@ class StickyProjectionWrapper {
203
215
  private applyProps(): void {
204
216
  const props = this.props();
205
217
  const reduced = reduceProps(props);
218
+ // Thunk: applyProps is on the scroll-driven path (every animated-tick while the pin is
219
+ // JS-driven), and JSON.stringify of a reduced transform is not free.
220
+ dlog(
221
+ () =>
222
+ `STICKY[wrap] index=${this.childIndex} applyProps translateY=${String(this.state.translateY)} ` +
223
+ `measured=${this.state.measured} layoutY=${this.state.layoutY} ` +
224
+ `scrollValueIsNative=${this.controller.config.scrollAnimatedValue.__isNative()} ` +
225
+ `reducedTransform=${JSON.stringify(reduced.transform ?? reduced.style)}`,
226
+ );
206
227
  for (const [key, value] of Object.entries(reduced)) routeProp(this.node, key, value);
207
228
 
208
229
  const next = new AnimatedProps(props);
209
230
  next.__attach();
231
+ // No explicit __makeNative() here on purpose: __attach joins the leaf to the interpolation,
232
+ // which sits under the shared scroll value, and AnimatedWithChildren.__addChild promotes a
233
+ // child joining an ALREADY-native parent (graph.ts). So the pin is native exactly when the
234
+ // scroll value is - logged below, because "is this actually on the UI thread" is otherwise
235
+ // invisible until you read three files.
236
+ dlog(
237
+ () =>
238
+ `STICKY[wrap] index=${this.childIndex} leaf attached isNative=${next.__isNative()} ` +
239
+ `interpolationIsNative=${this.animatedTranslateY.__isNative()} ` +
240
+ `scrollValueIsNative=${this.controller.config.scrollAnimatedValue.__isNative()}`,
241
+ );
210
242
  if (this.animatedProps !== undefined) this.animatedProps.__detach();
211
243
  this.animatedProps = next;
212
244
  this.cancelBind?.();
213
- this.cancelBind = whenCommitted(this.node, () => next.setNativeView(this.node));
245
+ this.cancelBind = whenCommitted(this.node, () => {
246
+ next.setNativeView(this.node);
247
+ dlog(`STICKY[wrap] index=${this.childIndex} setNativeView isNative=${next.__isNative()}`);
248
+ });
214
249
  }
215
250
  }
216
251
 
@@ -220,6 +255,13 @@ export class ScrollViewProjectionController {
220
255
  private readonly records: IProjectedRecord[] = [];
221
256
  private readonly headerLayoutYs = new Map<number, number>();
222
257
 
258
+ // Diagnostic-only: one ScrollView can host several projection controllers over its lifetime,
259
+ // and an app has several ScrollViews at once. Without an id every log line below is
260
+ // indistinguishable between instances, which is what made a "logs never stop" report
261
+ // impossible to read as either a real loop or two lists warming up in turn.
262
+ private readonly instanceId = ++projectionControllerSeq;
263
+ private reconcileSeq = 0;
264
+
223
265
  constructor(config: IScrollViewProjectionConfig) {
224
266
  this.config = config;
225
267
  }
@@ -305,10 +347,13 @@ export class ScrollViewProjectionController {
305
347
  nextStickyHeaderY(index: number): number | undefined {
306
348
  const stickyIndices = this.config.stickyHeaderIndices ?? [];
307
349
  const next = stickyIndices.find(entry => entry > index);
308
- return next === undefined ? undefined : this.headerLayoutYs.get(next);
350
+ const y = next === undefined ? undefined : this.headerLayoutYs.get(next);
351
+ dlog(`STICKY[proj] nextStickyHeaderY(index=${index}) next=${next} y=${y}`);
352
+ return y;
309
353
  }
310
354
 
311
355
  recordHeaderLayoutY(index: number, y: number): void {
356
+ dlog(`STICKY[proj] recordHeaderLayoutY index=${index} y=${y}`);
312
357
  if (this.headerLayoutYs.get(index) === y) return;
313
358
  this.headerLayoutYs.set(index, y);
314
359
  for (const record of this.records) record.sticky?.rebuild();
@@ -322,7 +367,8 @@ export class ScrollViewProjectionController {
322
367
  ): void {
323
368
  const before = beforeRecord === undefined ? undefined : directNode(beforeRecord);
324
369
  dlog(
325
- `Angular ScrollView projection insertRecord physicalParent=${parent.component} child=${record.child.component} before=${before ? before.component : 'undefined(append)'} parentIsContentNode=${parent === this.contentNode}`,
370
+ `STICKY[proj#${this.instanceId}] insertRecord child=${record.child.component} ` +
371
+ `before=${before ? before.component : 'append'} parentIsContentNode=${parent === this.contentNode}`,
326
372
  );
327
373
  if (before === undefined) insert(parent, record.child);
328
374
  else insert(parent, record.child, before);
@@ -330,26 +376,36 @@ export class ScrollViewProjectionController {
330
376
 
331
377
  private reconcileStickyRecords(): void {
332
378
  if (this.contentNode === undefined) return;
379
+ const seq = ++this.reconcileSeq;
333
380
  dlog(
334
- `Angular ScrollView projection reconcile contentNode=${this.contentNode.component} records=${this.records.length} contentNodeActualChildren=${this.contentNode.children.length}`,
381
+ () =>
382
+ `STICKY[proj#${this.instanceId}] reconcile#${seq} records=${this.records.length} ` +
383
+ `children=${this.contentNode?.children.length} sticky=${JSON.stringify(this.config.stickyHeaderIndices ?? [])}`,
335
384
  );
336
385
  const stickyIndices = new Set(this.config.stickyHeaderIndices ?? []);
337
386
  if (this.config.customStickyHeaderComponent !== undefined && stickyIndices.size > 0) {
338
387
  dlog(
339
- 'Angular ScrollView projection uses built-in sticky wrapper; custom StickyHeaderComponent is explicit-composition only',
388
+ 'STICKY[proj] uses built-in sticky wrapper; custom StickyHeaderComponent is explicit-composition only',
340
389
  );
341
390
  }
342
391
 
343
392
  let paintIndex = 0;
344
393
  for (const record of [...this.records]) {
345
394
  if (this.config.excludeRefreshControl && isProjectedRefreshControl(record.child)) {
346
- this.removeProjectedChild(record.child, (parent, child) => removeChild(parent, child));
395
+ // Removed INLINE, not through removeProjectedChild: that method ends with its own
396
+ // reconcileStickyRecords(), so calling it from inside this walk re-enters the walk while
397
+ // this.records is being spliced under it - one refresh control could then drive an
398
+ // unbounded recursion. Same removal, without the re-entry.
399
+ this.dropRecord(record);
347
400
  continue;
348
401
  }
349
402
  const countsAsChild = !isAnchor(record.child) && !isProjectedRefreshControl(record.child);
350
403
  const childIndex = paintIndex;
351
404
  if (countsAsChild) paintIndex += 1;
352
405
  const shouldWrap = countsAsChild && stickyIndices.has(childIndex);
406
+ // Deliberately NOT logged per child per walk: reconcile runs on every projected insert, so
407
+ // that shape is O(children x inserts) and buries the few lines that carry information. Only
408
+ // a state TRANSITION is logged, below.
353
409
  if (shouldWrap && record.wrapper === undefined) this.wrapRecord(record, childIndex);
354
410
  else if (!shouldWrap && record.wrapper !== undefined) this.unwrapRecord(record);
355
411
  else if (shouldWrap && record.stickyIndex !== childIndex) {
@@ -360,6 +416,18 @@ export class ScrollViewProjectionController {
360
416
  }
361
417
  }
362
418
 
419
+ // The removal half of removeProjectedChild, without its trailing reconcile - safe to call from
420
+ // inside the reconcile walk itself.
421
+ private dropRecord(record: IProjectedRecord): void {
422
+ if (this.contentNode === undefined) return;
423
+ record.sticky?.destroy();
424
+ projectedWrappers.delete(record.child);
425
+ const direct = directNode(record);
426
+ if (direct.parent === this.contentNode) removeChild(this.contentNode, direct);
427
+ const index = this.records.indexOf(record);
428
+ if (index !== -1) this.records.splice(index, 1);
429
+ }
430
+
363
431
  // Auto projection runs after Angular has already created projected child host nodes. At this
364
432
  // renderer boundary we can AOT-safely rearrange engine nodes, but we cannot instantiate an
365
433
  // arbitrary Angular component class as a wrapper without owning a ViewContainerRef/injector and
@@ -383,10 +451,14 @@ export class ScrollViewProjectionController {
383
451
  record.sticky = new StickyProjectionWrapper(this, childIndex, wrapper);
384
452
  record.stickyIndex = childIndex;
385
453
  projectedWrappers.set(record.child, record);
454
+ dlog(
455
+ `STICKY[proj#${this.instanceId}] WRAP child=${record.child.component} paintIndex=${childIndex}`,
456
+ );
386
457
  }
387
458
 
388
459
  private unwrapRecord(record: IProjectedRecord): void {
389
460
  if (this.contentNode === undefined || record.wrapper === undefined) return;
461
+ dlog(`STICKY[proj#${this.instanceId}] UNWRAP child=${record.child.component}`);
390
462
  record.sticky?.destroy();
391
463
  record.sticky = undefined;
392
464
  record.stickyIndex = undefined;