@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,33 +1,35 @@
1
- // ScrollView, the Angular lifecycle half. The Fabric tree is nested: a scroll view
2
- // wraps a content view that holds the children (RN's ScrollView.js shape). The platform-invariant
3
- // math (decelerationRate, the per-axis intrinsics/base style, the content-size dedupe, the
4
- // imperative handle, the native sticky scroll-attach, the aria/role fold) lives in
5
- // @symbiote-native/components, shared verbatim with React/Vue. Here Angular supplies only the lifecycle:
6
- // the host node held by IDENTITY through a directive's ElementRef, a forwarded-prop bag set onto
7
- // that node through Renderer2 (-> routeProp), the imperative handle (scrollTo/scrollToEnd/
8
- // flashScrollIndicators), the content-size dedupe, and the native sticky scroll-attach wired
9
- // through whenCommitted. This is the Angular twin of the React useRef + Vue shallowRef host.
1
+ // ScrollView, the Angular lifecycle half. The Fabric tree is nested: a scroll view wraps a
2
+ // content view holding the children (RN's ScrollView.js shape). The platform-invariant math
3
+ // (decelerationRate, per-axis intrinsics, content-size dedupe, imperative handle, native sticky
4
+ // scroll-attach, aria/role fold) lives in @symbiote-native/components, shared with React/Vue.
5
+ // Angular supplies only the lifecycle: the host node held by IDENTITY through a directive's
6
+ // ElementRef, a forwarded-prop bag set via Renderer2 (-> routeProp), and the native sticky attach
7
+ // wired through whenCommitted.
10
8
  //
11
- // What diverges per platform, how a RefreshControl integrates, stays in the
12
- // .ios/.android files (Metro filename-selected): iOS renders the RefreshControl as a SIBLING
13
- // before the content container; on Android an AndroidSwipeRefreshLayout WRAPS the scroll view.
9
+ // RefreshControl integration diverges per platform (.ios/.android, Metro filename-selected): iOS
10
+ // renders it as a SIBLING before the content container; Android WRAPS the scroll view in an
11
+ // AndroidSwipeRefreshLayout.
14
12
  //
15
- // Angular cannot transform <ng-content> children in the template the way React children.map /
16
- // Vue slots() can, so ScrollView owns a tiny projection bridge: the content host registers a
17
- // ScrollViewProjectionController with the Angular renderer, and the renderer wraps direct projected
18
- // children whose indices match stickyHeaderIndices. Android RefreshControl projection is handled by
19
- // re-rendering the projected component's native prop surface as the wrapper around the scroll view
20
- // (RN's Android shape), while excluding the original projected node from the content slot.
13
+ // Angular cannot transform <ng-content> children the way React children.map / Vue slots() can, so
14
+ // ScrollView owns a projection bridge: the content host registers a ScrollViewProjectionController
15
+ // with the Angular renderer, which wraps projected children whose indices match
16
+ // stickyHeaderIndices. Android RefreshControl projection re-renders the projected component as the
17
+ // wrapper around the scroll view (RN's Android shape), excluding it from the content slot.
21
18
 
22
19
  import {
23
20
  Directive,
21
+ computed,
22
+ signal,
24
23
  ElementRef,
25
24
  EventEmitter,
26
25
  Output,
27
26
  ViewChild,
28
27
  ContentChild,
29
28
  inject,
29
+ type AfterContentChecked,
30
30
  type AfterViewInit,
31
+ type DoCheck,
32
+ type OnChanges,
31
33
  type OnDestroy,
32
34
  } from '@angular/core';
33
35
  import {
@@ -60,7 +62,7 @@ import {
60
62
  type ISymbioteNode,
61
63
  type IViewStyle,
62
64
  } from '@symbiote-native/engine';
63
- import { SymbioteHostPropsDirective } from '../../primitives';
65
+ import { anchorHostStyle, SymbioteHostPropsDirective } from '../../primitives';
64
66
  import { RefreshControl } from '../refresh-control';
65
67
  import { ScrollViewProjectionController } from './projection';
66
68
 
@@ -158,12 +160,10 @@ export const SCROLL_VIEW_INPUTS = [
158
160
  'aria-valuetext',
159
161
  ];
160
162
 
161
- // The Angular-facing prop surface. React's IScrollViewProps is React-coupled (ReactNode children,
162
- // ReactElement refreshControl); Angular takes children via <ng-content> and composes the
163
- // RefreshControl through projection / an outer wrap, so this mirrors the same pass-through surface
164
- // minus those, declared per-adapter over the shared accessibility base since a framework element/ref
165
- // field can't live in a shared agnostic type. Every prop is accepted and typed against the full
166
- // React surface so app code type-checks against parity now.
163
+ // The Angular-facing prop surface: React's IScrollViewProps minus the React-coupled fields
164
+ // (ReactNode children, ReactElement refreshControl) Angular takes children via <ng-content> and
165
+ // composes RefreshControl through projection instead. Declared per-adapter over the shared
166
+ // accessibility base since a framework element/ref field can't live in a shared agnostic type.
167
167
  export interface IAngularScrollViewProps extends IAccessibilityProps, IAriaProps {
168
168
  style?: IStyleProp<IViewStyle>;
169
169
  // A bare string resolves through the shared style registry, like `class` on the host node.
@@ -287,7 +287,15 @@ export class ScrollViewProjectionDirective {
287
287
  // ngtsc rejects the inherited hooks/queries (NG2007). The concrete platform @Components add ONLY a
288
288
  // decorator + their platform template; all behavior lives here.
289
289
  @Directive()
290
- export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterViewInit, OnDestroy {
290
+ export abstract class ScrollViewBase
291
+ implements
292
+ IAngularScrollViewInputs,
293
+ AfterContentChecked,
294
+ AfterViewInit,
295
+ DoCheck,
296
+ OnChanges,
297
+ OnDestroy
298
+ {
291
299
  style: IStyleProp<IViewStyle> | undefined;
292
300
  contentContainerStyle: IStyleProp<IViewStyle> | string | undefined;
293
301
  horizontal: boolean | undefined;
@@ -393,6 +401,20 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
393
401
 
394
402
  @ContentChild(RefreshControl) protected projectedRefreshControl?: RefreshControl;
395
403
 
404
+ // This component's OWN anchor host — where a `class="..."` at the use site resolves (see
405
+ // anchorHostStyle's doc comment) — NOT `#host` in the platform templates, which targets the real
406
+ // inner scroll-view primitive one level down. Injected in the BASE because both platform
407
+ // subclasses merged the identical anchor style into their `scrollProps` override.
408
+ private readonly elementRef = inject<ElementRef<unknown>>(ElementRef);
409
+
410
+ // The anchor's class-derived style is written by the RENDERER (addClass/removeClass ->
411
+ // commitClassStyle), never through an @Input, so `inputsRevision` cannot cover it and a bag
412
+ // memoized on plain reads of it would keep a stale class forever. It gets its own signal, polled
413
+ // in ngDoCheck below: that hook runs on exactly the checks that could have changed it (Angular
414
+ // applies the parent's class bindings before flushing this directive's pre-order hooks), which is
415
+ // the same cadence the old `scrollProps` getter was re-read at.
416
+ protected readonly anchorStyle = signal<unknown>(undefined);
417
+
396
418
  // A single AnimatedValue tracks the scroll offset and drives every sticky header's translateY
397
419
  // (RN's _scrollAnimatedValue). A stable field (allocated once, held by identity — Angular does
398
420
  // not proxy class fields, so no markRaw needed); the native attach below feeds it on the UI
@@ -404,6 +426,9 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
404
426
  private lastContentSize: IContentSize | null = null;
405
427
  // Inverted sticky headers stick to the BOTTOM, so they need the viewport height (RN _handleLayout).
406
428
  private viewportHeight: number | undefined = undefined;
429
+ // What the projection controller was last told about the projected RefreshControl, so
430
+ // ngAfterContentChecked can tell a real content change from an ordinary check.
431
+ private lastHasProjectedRefreshControl = false;
407
432
 
408
433
  protected readonly projectionController = new ScrollViewProjectionController({
409
434
  stickyHeaderIndices: undefined,
@@ -421,6 +446,9 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
421
446
  // bind runs now if committed, else after the commit that assigns the tag. Detached on destroy.
422
447
  private detachStickyScroll: (() => void) | undefined;
423
448
  private cancelStickyBind: (() => void) | undefined;
449
+ // What the current attach was made for, so a re-run that changes nothing stays a no-op.
450
+ private stickyAttachedEnabled = false;
451
+ private stickyAttachedNode: ISymbioteNode | null = null;
424
452
 
425
453
  private lastLoggedIsHorizontal: boolean | undefined = undefined;
426
454
 
@@ -433,7 +461,14 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
433
461
  // (see AnimatedComponentBase.reconcile()'s identical warning).
434
462
  private readonly handleInvertedStickyLayout = (event: ISymbioteEvent): void => {
435
463
  const height = readLayoutDimension(event, 'height');
436
- if (height !== undefined) this.viewportHeight = height;
464
+ // The projection controller is pushed from here rather than picked up on the next prop-bag
465
+ // read: viewportHeight is internal mutable state, so once `scrollProps` became a memoized
466
+ // computed nothing would re-read it. Guarded on a real change - an onLayout that reports the
467
+ // same height must not cost a sticky reconcile walk.
468
+ if (height !== undefined && height !== this.viewportHeight) {
469
+ this.viewportHeight = height;
470
+ this.updateProjectionController();
471
+ }
437
472
  this.layout.emit(event);
438
473
  };
439
474
 
@@ -449,14 +484,11 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
449
484
  this.contentSizeChange.emit({ width, height });
450
485
  };
451
486
 
452
- // Memoizes the wrapper each `emitterCallback` builds, keyed by the emitter it closes over
453
- // (identity holds across ScrollViewBase's own @Output()s and a projected RefreshControl's,
454
- // since a WeakMap key is just object identity). Without this, `scrollProps`/`refreshControlProps`
455
- // handed a fresh `(event) => emitter.emit(event)` closure to the forwarded prop bag on every
456
- // evaluation the same jsonEqual/Object.is function-leaf problem as the arrow fields above, one
457
- // level further removed. `.observed` is checked BEFORE the cache so an unbound event still
458
- // resolves to `undefined` (the "nobody cares" contract `emitterCallback`'s own comment documents),
459
- // not a stale cached wrapper from before the last subscriber unsubscribed.
487
+ // Memoizes the wrapper each `emitterCallback` builds, keyed by the emitter it closes over.
488
+ // Without this, `scrollProps`/`refreshControlProps` would hand a fresh closure to the forwarded
489
+ // prop bag on every evaluation the same jsonEqual/Object.is function-leaf problem as the arrow
490
+ // fields above. `.observed` is checked BEFORE the cache so an unbound event still resolves to
491
+ // `undefined` rather than a stale wrapper from before the last unsubscribe.
460
492
  private readonly emitterCallbackCache = new WeakMap<
461
493
  EventEmitter<ISymbioteEvent>,
462
494
  IScrollHandler
@@ -500,12 +532,35 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
500
532
  .scrollViewBaseStyle;
501
533
  }
502
534
 
535
+ // Bridges non-reactive @Input fields into the reactive graph, so a computed() can memoize a
536
+ // bag derived from them. Signal inputs would make this unnecessary, but `input()` is visible
537
+ // only to the AOT compiler and this package's unit suite runs on JIT - see the
538
+ // `angular-adapter-change-detection` skill, §6. `signal()`/`computed()` themselves are plain
539
+ // runtime APIs and need no compiler support, which is what makes this bridge possible at all.
540
+ //
541
+ // ONLY safe for a bag whose every dependency is an @Input. A bag that also reads internal
542
+ // mutable state (viewportHeight, lastContentSize, the anchor's class-derived style) would go
543
+ // stale, because nothing bumps this for those - make that state its own signal (see
544
+ // `anchorStyle`) instead of widening this one.
545
+ private readonly inputsRevision = signal(0);
546
+
503
547
  // The forwarded host-prop bag for the scroll-view node: aria/role folded, the native pass-through
504
548
  // families, the scroll-event callbacks, then the lifecycle-managed overrides (nestedScrollEnabled
505
549
  // default ON, horizontal when defined, decelerationRate resolved per-platform, the sticky onScroll
506
550
  // path). Base style UNDER user style so an explicit user value still wins. Mirrors Vue's outerProps
507
551
  // + scrollProps assembly.
508
- get scrollProps(): Record<string, unknown> {
552
+ //
553
+ // MEASURED 2026-08-16: as a getter this rebuilt the whole bag on every refresh of this view, so
554
+ // the `symbioteHostProps` reference check failed every time and every key was re-pushed through
555
+ // the renderer - once per scroll frame per host binding (2 before `contentProps` was memoized,
556
+ // 1 after, 0 now; the pin lives in prop-bag-stability.test.ts).
557
+ //
558
+ // It is a `computed` on the BASE and no longer overridable: a subclass getter cannot override a
559
+ // field. The platform subclasses used to override it with byte-identical bodies (merge the
560
+ // anchor's class-derived style ahead of the resolved style), so that merge moved down here
561
+ // rather than into a `decorateScrollProps` hook both of them would implement the same way.
562
+ readonly scrollProps = computed<Record<string, unknown>>(() => {
563
+ this.inputsRevision();
509
564
  const bag: Record<string, unknown> = compact({
510
565
  ...resolveAccessibilityProps(this.accessibilityInputs()),
511
566
  scrollEnabled: this.scrollEnabled,
@@ -563,14 +618,11 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
563
618
  }
564
619
 
565
620
  // onScroll: when sticky headers are active, the offset must reach the AnimatedValue (RN
566
- // _scrollAnimatedValueAttachment). With the native module, the value is driven on the UI thread
567
- // by the post-commit attach below, so onScroll forwards to the user with throttle 1
568
- // (ScrollView.js:1798). Without it, Animated.event drives the value each frame (the JS jitter)
569
- // and forwards the user handler as the listener passthrough.
570
- // The scroll-forwarding DECISIONS (which onScroll path, the 1/16 throttle defaults, whether to
571
- // capture the viewport height) are folded out to the shared resolveScrollForwarding; here Angular
572
- // only EXECUTES them, keeping its stable-reference handlers (stickyOnScrollHandler /
573
- // handleInvertedStickyLayout / emitterCallback) so a fresh closure never forces a re-clone cascade.
621
+ // _scrollAnimatedValueAttachment). With the native module, the post-commit attach below drives
622
+ // it on the UI thread (throttle 1, ScrollView.js:1798); without it, Animated.event drives the
623
+ // value each frame (JS jitter). The DECISIONS (which path, throttle defaults, whether to
624
+ // capture viewport height) are folded out to the shared resolveScrollForwarding; Angular only
625
+ // EXECUTES them, keeping stable-reference handlers so a fresh closure never forces a re-clone.
574
626
  const nativeStickyAvailable = this.hasStickyHeaders && isNativeAnimatedAvailable();
575
627
  const forwarding = resolveScrollForwarding({
576
628
  hasStickyHeaders: this.hasStickyHeaders,
@@ -599,41 +651,62 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
599
651
  if (layoutCallback !== undefined) bag.onLayout = layoutCallback;
600
652
  }
601
653
 
654
+ // Thunks, not strings: an eagerly built message would be paid with logging off (see dlog's own
655
+ // comment). Less often than it used to fire now that this bag is memoized, but the recompute
656
+ // still rides every real input change.
657
+ dlog(
658
+ () =>
659
+ `Angular ScrollView -> ${this.isHorizontal ? 'horizontal' : 'vertical'} (sticky=${this.hasStickyHeaders})`,
660
+ );
602
661
  dlog(
603
- `Angular ScrollView -> ${this.isHorizontal ? 'horizontal' : 'vertical'} (sticky=${this.hasStickyHeaders})`,
662
+ () =>
663
+ `STICKY[sv] forwarding mode=${forwarding.mode} throttle=${forwarding.scrollEventThrottle} ` +
664
+ `nativeStickyAvailable=${nativeStickyAvailable} onScrollBound=${bag.onScroll !== undefined}`,
604
665
  );
605
666
 
606
- bag.style = [this.scrollViewBaseStyle, this.style];
607
- this.updateProjectionController();
667
+ // The anchor's class-derived style goes FIRST: flattenStyle's later-wins collapse keeps an
668
+ // explicit [style] winning over the ambient class. Flat rather than the nested
669
+ // `[anchor, [base, style]]` the platform overrides used to build - flattenStyle folds nested
670
+ // arrays in the same order, so the resolved value is unchanged.
671
+ bag.style = [this.anchorStyle(), this.scrollViewBaseStyle, this.style];
608
672
  return bag;
609
- }
673
+ });
610
674
 
611
675
  // Overridable hook, NOT `this.style` directly: `splitLayoutProps` below decides which layout
612
676
  // properties (flex/height/gap/…) go on the Android outer refresh-control wrapper vs. the inner
613
- // scroll view. `this.style` alone only ever carries the explicit `[style]` @Input — a composed
614
- // component's OWN anchor host (see anchorHostStyle's doc comment in primitives/shared.ts) can
615
- // hold ADDITIONAL class-derived layout style that's otherwise invisible to this split, so its
616
- // wrapper never gets its needed layout (collapses to zero size the Android outer wrapper
617
- // shows literally nothing on screen). This mirrors the Vue adapter's identical
618
- // `layoutSplitStyle` field and the real Android device bug it fixes; the Angular Android
619
- // ScrollView (index.android.ts) overrides this to merge in `anchorHostStyle`. Default here is
620
- // `this.style` alone, preserving current behavior for anything that doesn't override it.
677
+ // scroll view. `this.style` alone only carries the explicit `[style]` @Input — a composed
678
+ // component's own anchor host can hold ADDITIONAL class-derived layout style invisible to this
679
+ // split, so the wrapper collapses to zero size on real Android devices. Angular's Android
680
+ // ScrollView (index.android.ts) overrides this to merge in `anchorHostStyle` (mirrors Vue's
681
+ // identical `layoutSplitStyle` field and the device bug it fixes).
621
682
  protected get layoutSplitStyle(): IStyleProp<IViewStyle> {
622
683
  return this.style;
623
684
  }
624
685
 
625
- get androidWrappedScrollProps(): Record<string, unknown> {
626
- const props = { ...this.scrollProps };
686
+ // Memoizable for the same reasons `scrollProps` is: `layoutSplitStyle` resolves to @Input `style`
687
+ // plus (on Android) the `anchorStyle` signal, both covered. The anchor style is deliberately NOT
688
+ // re-merged into `props.style` here - `splitLayoutProps` already routed its layout half to the
689
+ // outer refresh-control wrapper and left the rest in `inner`.
690
+ readonly androidWrappedScrollProps = computed<Record<string, unknown>>(() => {
691
+ this.inputsRevision();
692
+ const props = { ...this.scrollProps() };
627
693
  const { inner, outer } = splitLayoutProps(this.layoutSplitStyle);
628
694
  dlog(
629
- `Angular ScrollView splitProbe layoutSplitStyle=${JSON.stringify(this.layoutSplitStyle)} ` +
695
+ () =>
696
+ `Angular ScrollView splitProbe layoutSplitStyle=${JSON.stringify(this.layoutSplitStyle)} ` +
630
697
  `outer=${JSON.stringify(outer)} inner=${JSON.stringify(inner)}`,
631
698
  );
632
699
  props.style = [this.scrollViewBaseStyle, inner];
633
700
  props.nestedScrollEnabled = true;
634
701
  return props;
635
- }
702
+ });
636
703
 
704
+ // Deliberately still GETTERS, unlike every other bag on this class. Their whole content comes
705
+ // from the PROJECTED RefreshControl's own @Input()s (refreshing, tintColor, colors, …), which
706
+ // Angular routes through THAT component's ngOnChanges - `inputsRevision` here never moves for
707
+ // them. Memoizing on it would freeze `refreshing` at its first value and strand the native
708
+ // spinner forever, which is exactly the silent-staleness class this file avoids elsewhere.
709
+ // Making them memoizable means giving RefreshControl its own revision signal to read here.
637
710
  get iosRefreshControlProps(): Record<string, unknown> {
638
711
  return this.refreshControlProps();
639
712
  }
@@ -641,7 +714,8 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
641
714
  get androidRefreshControlProps(): Record<string, unknown> {
642
715
  const { outer } = splitLayoutProps(this.layoutSplitStyle);
643
716
  dlog(
644
- `Angular ScrollView refreshControlProbe outer=${JSON.stringify(outer)} ` +
717
+ () =>
718
+ `Angular ScrollView refreshControlProbe outer=${JSON.stringify(outer)} ` +
645
719
  `hasRefresh=${this.projectedRefreshControl !== undefined}`,
646
720
  );
647
721
  return this.refreshControlProps(outer);
@@ -685,7 +759,8 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
685
759
  // children of a scroll view that hosts exactly one — an addViewAt crash). collapsableChildren
686
760
  // false preserves the cell views maintainVisibleContentPosition/snapToAlignment anchor against.
687
761
  // onLayout synthesizes onContentSizeChange (deduped). iOS never flattens; both are no-ops there.
688
- get contentProps(): Record<string, unknown> {
762
+ readonly contentProps = computed<Record<string, unknown>>(() => {
763
+ this.inputsRevision();
689
764
  const { contentStyle } = selectScrollIntrinsics(
690
765
  this.isHorizontal,
691
766
  this.resolvedContentContainerStyle,
@@ -696,13 +771,18 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
696
771
  }
697
772
  bag.onLayout = this.handleContentLayout;
698
773
  return bag;
699
- }
774
+ });
700
775
 
701
776
  // Wraps an @Output() as a plain callback only while it has a subscriber, so an unbound event
702
777
  // still leaves the forwarded prop bag key absent — the same "undefined means nobody cares"
703
778
  // contract the old @Input() callbacks had. Mirrors Pressable's emitterHandler. The wrapper itself
704
779
  // is memoized per emitter (emitterCallbackCache) rather than rebuilt on every call: see that
705
780
  // field's comment for why an unstable closure here breaks the forwarded prop bag.
781
+ //
782
+ // `.observed` is NOT reactive, and `scrollProps` memoizes over it. That is sound because Angular
783
+ // subscribes a template's (event) listeners in its CREATION pass, before any input is written and
784
+ // before this component's own view is first refreshed - so `observed` is already settled at the
785
+ // first evaluation and cannot flip afterwards without destroying the whole component.
706
786
  private emitterCallback(emitter: EventEmitter<ISymbioteEvent>): IScrollHandler | undefined {
707
787
  if (!emitter.observed) return undefined;
708
788
  let cached = this.emitterCallbackCache.get(emitter);
@@ -762,23 +842,98 @@ export abstract class ScrollViewBase implements IAngularScrollViewInputs, AfterV
762
842
  }
763
843
 
764
844
  ngAfterViewInit(): void {
845
+ dlog('STICKY[sv] ngAfterViewInit');
846
+ // Runs once the template has bound the content node, so this is the reconcile that actually
847
+ // wraps the sticky children (the ngOnChanges one above fires before the content node exists).
848
+ this.updateProjectionController();
849
+ this.attachSticky();
850
+ }
851
+
852
+ // Re-read the anchor's class-derived style. It is written by the renderer's addClass/removeClass,
853
+ // never through an @Input, so this poll is the only thing that can invalidate a bag holding it -
854
+ // ngOnChanges alone would strand a class toggled after mount (covered by
855
+ // scroll-view-class-style.test.ts's toggle case, which goes red the moment this hook stops
856
+ // polling). ngDoCheck is the right hook, not a near-enough one: render3's `callHooks` flushes a
857
+ // node's pre-order hooks at the next `ɵɵadvance` PAST it, which is after the parent wrote this
858
+ // element's `ɵɵclassProp` and before this component's own view refreshes. It also runs under
859
+ // `setActiveConsumer(null)`, so the write registers no dependency on the caller's view and cannot
860
+ // throw NG0600. And `commitClassStyle` allocates a fresh style array only when a class token
861
+ // actually moved, so `signal.set`'s own Object.is check makes an unchanged poll dirty nothing.
862
+ ngDoCheck(): void {
863
+ this.anchorStyle.set(anchorHostStyle(this.elementRef));
864
+ }
865
+
866
+ // The projected RefreshControl is a @ContentChild, not an @Input, so ngOnChanges never fires when
867
+ // one is added or removed at the use site - and `excludeRefreshControl` decides whether the
868
+ // projection walk pulls it out of the scroll content. Angular resolves the query before this
869
+ // hook. Guarded on the flag itself so a check that changed nothing skips the reconcile walk,
870
+ // which the old getter-driven update could not do.
871
+ ngAfterContentChecked(): void {
872
+ if (this.hasProjectedRefreshControl === this.lastHasProjectedRefreshControl) return;
873
+ this.lastHasProjectedRefreshControl = this.hasProjectedRefreshControl;
874
+ this.updateProjectionController();
875
+ }
876
+
877
+ // `stickyHeaderIndices` is an ordinary @Input, so it can arrive AFTER the first change-detection
878
+ // pass - any app deriving it from data rather than writing a literal in the template does exactly
879
+ // that. ngAfterViewInit runs once, so without this the native attach below would be skipped on
880
+ // that first pass and never retried: the wrapper still projects (that half self-heals), the
881
+ // header sits at the right place with the right z-index, and simply never moves. Device-reported
882
+ // 2026-08-16, regression-covered in sticky-native-attach.test.ts. The other three adapters never
883
+ // had this hole because they re-run the attach from a reactive effect keyed on the same
884
+ // condition (Svelte's `$effect` in index.svelte, React's useEffect deps, Vue's watcher).
885
+ ngOnChanges(): void {
886
+ dlog('STICKY[sv] ngOnChanges');
887
+ // Every computed() prop bag below reads plain @Input fields, which are NOT reactive on their
888
+ // own - this bump is what tells them an input changed. It must stay in ngOnChanges: that is
889
+ // the single moment Angular has finished writing every changed input for this pass.
890
+ this.inputsRevision.update(revision => revision + 1);
891
+ // stickyHeaderIndices / invertStickyHeaders / StickyHeaderComponent are @Inputs, so this is
892
+ // where the projection controller learns about them. It used to be a side effect of reading
893
+ // the `scrollProps` getter - which a memoized computed no longer performs per pass.
765
894
  this.updateProjectionController();
766
895
  this.attachSticky();
767
896
  }
768
897
 
769
- // Wire the native sticky-scroll attach after the view exists. Re-attaches if the node identity
770
- // changes; no-op when sticky is off or the native module is absent.
898
+ // Wire the native sticky-scroll attach once the view exists AND sticky is actually on. Re-runs
899
+ // on every input change, so it must be idempotent: see the guard below.
771
900
  private attachSticky(): void {
901
+ const wantsAttach = this.hasStickyHeaders && isNativeAnimatedAvailable();
902
+ const node = wantsAttach ? this.hostNode : null;
903
+ dlog(
904
+ () =>
905
+ `STICKY[sv] attachSticky indices=${JSON.stringify(this.stickyHeaderIndices)} ` +
906
+ `hasSticky=${this.hasStickyHeaders} nativeAvailable=${isNativeAnimatedAvailable()} ` +
907
+ `wantsAttach=${wantsAttach} hasNode=${node !== null} ` +
908
+ `wasEnabled=${this.stickyAttachedEnabled} nodeChanged=${node !== this.stickyAttachedNode}`,
909
+ );
910
+ // Nothing that decides the attach has changed - return rather than detach and rebind the
911
+ // native scroll event, which every unrelated @Input change would otherwise churn now that
912
+ // ngOnChanges drives this too.
913
+ if (wantsAttach === this.stickyAttachedEnabled && node === this.stickyAttachedNode) {
914
+ dlog('STICKY[sv] attachSticky skipped (nothing about the attach changed)');
915
+ return;
916
+ }
917
+
772
918
  this.cancelStickyBind?.();
773
919
  this.cancelStickyBind = undefined;
774
920
  if (this.detachStickyScroll !== undefined) {
775
921
  this.detachStickyScroll();
776
922
  this.detachStickyScroll = undefined;
777
923
  }
778
- if (!this.hasStickyHeaders || !isNativeAnimatedAvailable()) return;
779
- const node = this.hostNode;
780
- if (node === null) return;
924
+ // Recorded even when the node is not resolved yet, so the NEXT call (which will see a real
925
+ // node) reads as a change and attaches.
926
+ this.stickyAttachedEnabled = wantsAttach;
927
+ this.stickyAttachedNode = node;
928
+ if (!wantsAttach || node === null) {
929
+ dlog(
930
+ `STICKY[sv] attachSticky NOT attaching (wantsAttach=${wantsAttach} hasNode=${node !== null})`,
931
+ );
932
+ return;
933
+ }
934
+ dlog('STICKY[sv] attachSticky waiting for commit to bind the native scroll event');
781
935
  this.cancelStickyBind = whenCommitted(node, () => {
936
+ dlog('STICKY[sv] attachSticky node committed -> attachStickyScroll');
782
937
  this.detachStickyScroll = attachStickyScroll(node, this.scrollAnimatedValue);
783
938
  });
784
939
  }
@@ -1,20 +1,15 @@
1
- // Sticky headers: the Angular twin of adapters/{react,vue}/src/components/scroll-view/
2
- // sticky-header.{tsx,ts}, the JS layer RN implements in ScrollView.js / ScrollViewStickyHeader.js.
3
- //
4
- // Source-based: RN does stickiness PURELY IN JS. A single `_scrollAnimatedValue` an Animated.event
5
- // drives from `onScroll` feeds each flagged header's translateY through an interpolation that pins
6
- // it to the top (or bottom, inverted) until the next header collides with it. The native Fabric
7
- // scroll view does NOT honor `stickyHeaderIndices` on its own. The load-bearing top/inverted
8
- // interpolation math (computeStickyInterpolation) lives framework-agnostic in @symbiote-native/components
9
- // this file holds the Angular component shell, the layout state, and the interpolation
10
- // build, sharing the math verbatim with React/Vue. Angular supplies only the lifecycle (inputs +
11
- // manual change detection instead of useState/useEffect or refs/watch).
1
+ // Sticky headers: RN does stickiness PURELY IN JS (ScrollView.js / ScrollViewStickyHeader.js).
2
+ // A single `_scrollAnimatedValue` an Animated.event drives from `onScroll` feeds each flagged
3
+ // header's translateY through an interpolation that pins it to the top (or bottom, inverted)
4
+ // until the next header collides with it the native Fabric scroll view does NOT honor
5
+ // `stickyHeaderIndices` on its own. The interpolation math (computeStickyInterpolation) lives
6
+ // framework-agnostic in @symbiote-native/components, shared verbatim with React/Vue; this file
7
+ // holds the Angular component shell, layout state, and interpolation build.
12
8
  //
13
9
  // This is the public sticky-header WRAPPER for explicit composition. ScrollView also auto-wraps
14
- // projected direct children named by stickyHeaderIndices through its projection bridge (see
15
- // projection.ts), because Angular templates cannot map <ng-content> children directly; that auto
16
- // path intentionally uses this built-in wrapper rather than dynamically instantiating arbitrary
17
- // custom component classes.
10
+ // projected children named by stickyHeaderIndices through its projection bridge (projection.ts),
11
+ // because Angular templates cannot map <ng-content> children directly; that auto path
12
+ // intentionally reuses this built-in wrapper rather than instantiating arbitrary component types.
18
13
 
19
14
  import {
20
15
  ChangeDetectionStrategy,
@@ -198,7 +193,7 @@ export class ScrollViewStickyHeader
198
193
  this.interpolation = next;
199
194
  this.animatedTranslateY = next;
200
195
  dlog(
201
- `Angular ScrollViewStickyHeader interpolation measured=${this.state.measured} y=${this.state.layoutY}`,
196
+ `STICKY[header] interpolation measured=${this.state.measured} y=${this.state.layoutY}`,
202
197
  );
203
198
  break;
204
199
  }
@@ -1,33 +1,27 @@
1
1
  // SectionList, the Angular public list-of-sections component. A thin convenience surface over
2
2
  // VirtualizedSectionList, mirroring RN's layering (SectionList -> VirtualizedSectionList ->
3
3
  // VirtualizedList). All section-flattening / windowing / sticky-header / imperative-scroll logic
4
- // lives below; this layer re-exposes the same surface under the SectionList name and re-exposes the
5
- // handle (delegating to the inner VirtualizedSectionList). The Angular twin of
6
- // adapters/vue/src/components/section-list/index.ts.
4
+ // lives below; this layer re-exposes the same surface under the SectionList name and delegates
5
+ // the handle to the inner VirtualizedSectionList.
7
6
  //
8
- // As in the Vue twin, SectionList is a PURE FORWARDER: its public prop surface IS
9
- // VirtualizedSectionList's (RN layers them one-for-one), and the SectionList-flavour defaults
10
- // (stickySectionHeadersEnabled per Platform.OS, the section entry keyExtractor) already live INSIDE
11
- // VirtualizedSectionList it applies `stickySectionHeadersEnabled ?? (Platform.OS === 'ios')` in
12
- // its ngDoCheck and routes keyExtractor through the shared sectionEntryKey. So SectionList forwards
13
- // each prop unchanged (undefined included) and the default lands one layer down; double-applying
14
- // here would fight VSL. Vue spreads $attrs to do this in one line — Angular has no attrs-spread, so
15
- // every prop is forwarded as an explicit @Input binding, except the seven list-lifecycle events
7
+ // SectionList is a PURE FORWARDER: its public prop surface IS VirtualizedSectionList's (RN layers
8
+ // them one-for-one), and the SectionList-flavour defaults (stickySectionHeadersEnabled per
9
+ // Platform.OS, the section entry keyExtractor) already live INSIDE VirtualizedSectionList — double-
10
+ // applying them here would fight it. Angular has no attrs-spread (unlike Vue's $attrs), so every
11
+ // prop is forwarded as an explicit @Input binding, except the seven list-lifecycle events
16
12
  // (endReached/startReached/refresh/accessibilityAction/accessibilityTap/magicTap/
17
13
  // accessibilityEscape), which are real @Output() EventEmitters re-emitted via a listener binding.
18
14
  //
19
- // TEMPLATE FORWARDING — RE-STAMP, the same pattern FlatList's single-column branch was fixed to use
20
- // (see flat-list/index.ts). A bare `<ng-content></ng-content>` passthrough does NOT let
21
- // VirtualizedSectionList's own @ContentChild resolve directives across the SECOND projection hop
22
- // (SectionList's own `<ng-content>` re-projecting content that was actually authored on `<SectionList>`
23
- // by the app, one level further out) — Angular's content queries resolve against what was projected
24
- // directly onto the querying component's OWN tag, not transitively through a nested `<ng-content>`
25
- // relay. So SectionList captures the app's `<ng-template vSectionItem>` / vSectionHeader /
26
- // vSectionFooter / vSectionSeparator / vListHeader / vListFooter / vListEmpty / vListSeparator with
27
- // its OWN @ContentChild (a single, direct projection hop this always resolves) and re-authors
28
- // equivalent `<ng-template>`s on `<VirtualizedSectionList>`, each forwarding the captured
29
- // templateRef + context through VListOutletDirective, exactly mirroring how VirtualizedSectionList
30
- // itself re-stamps its own captured directives onto its inner VirtualizedList.
15
+ // TEMPLATE FORWARDING — RE-STAMP, the same pattern FlatList's single-column branch uses (see
16
+ // flat-list/index.ts). A bare `<ng-content></ng-content>` passthrough does NOT let
17
+ // VirtualizedSectionList's own @ContentChild resolve directives across a SECOND projection hop
18
+ // Angular's content queries resolve only against what was projected directly onto the querying
19
+ // component's OWN tag. So SectionList captures the app's `<ng-template vSectionItem>` /
20
+ // vSectionHeader / vSectionFooter / vSectionSeparator / vListHeader / vListFooter / vListEmpty /
21
+ // vListSeparator with its OWN @ContentChild (a single, direct hop always resolves) and
22
+ // re-authors equivalent `<ng-template>`s on `<VirtualizedSectionList>`, forwarding the captured
23
+ // templateRef + context through VListOutletDirectivemirroring how VirtualizedSectionList itself
24
+ // re-stamps its own captured directives onto its inner VirtualizedList.
31
25
 
32
26
  import {
33
27
  ChangeDetectionStrategy,
@@ -62,7 +56,7 @@ import {
62
56
  type IVListSeparatorContext,
63
57
  } from '../virtualized-list';
64
58
  import { VListOutletDirective } from '../virtualized-list/directives';
65
- import { stableAnchorStyle } from '../../primitives';
59
+ import { stableAnchorStyle, SymbioteStyleInputDirective } from '../../primitives';
66
60
  import {
67
61
  VSectionFooterDirective,
68
62
  VSectionHeaderDirective,
@@ -126,6 +120,7 @@ export type ISectionListInputs<ItemT> = Omit<
126
120
  @Component({
127
121
  selector: 'SectionList',
128
122
  standalone: true,
123
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
129
124
  imports: [
130
125
  VirtualizedSectionList,
131
126
  VSectionItemDirective,
@@ -311,18 +306,14 @@ export class SectionList<ItemT = unknown>
311
306
  @Input() testID?: string;
312
307
  @Input() nativeID?: string;
313
308
 
314
- // Bound to the template's `[style]="resolvedStyle"`, which Angular compiles to the built-in
315
- // ɵɵstyleMap instruction — it only understands a flat object, never an array (RN's own
316
- // `style={[a, b]}` composition idiom crashes deep inside Angular's styling engine), so this
317
- // flattens `style` via the engine's own flattenStyle before it ever reaches that binding.
318
- // anchorHostStyle merges in this component's OWN anchor's class-derived style (see its doc
319
- // comment) `elementRef` below is SectionList's OWN host, not `list`'s inner VirtualizedSectionList.
320
- // A plain `flattenStyle([...])` here would allocate a FRESH object on every getter read (every CD
321
- // check), which bound onto the nested VirtualizedSectionList/VirtualizedList's own `@Input()
322
- // style` — defeats VirtualizedList's `ngDoCheck` dedup gate and free-runs change detection
323
- // forever (the exact bug `stableAnchorStyle` exists to prevent, see its doc comment and the
324
- // `flat-list-array-style.test.ts` regression it fixes). `cachedResolvedStyle` is the getter's own
325
- // persisted "previous" value across reads, since a getter has no natural field to compare against.
309
+ // Bound to `[style]="resolvedStyle"`, which Angular compiles to ɵɵstyleMap — it only
310
+ // understands a flat object, never an array (RN's `style={[a, b]}` idiom crashes deep inside
311
+ // Angular's styling engine), so this flattens `style` first, merging in this component's OWN
312
+ // anchor's class-derived style via anchorHostStyle (`elementRef` here is SectionList's own
313
+ // host, not `list`'s inner VirtualizedSectionList). A plain `flattenStyle([...])` would
314
+ // allocate a fresh object on every CD check, defeating VirtualizedList's `ngDoCheck` dedup gate
315
+ // and free-running change detection forever the bug `stableAnchorStyle` exists to prevent
316
+ // (see its doc comment). `cachedResolvedStyle` is the getter's own persisted "previous" value.
326
317
  private cachedResolvedStyle: Record<string, unknown> | undefined;
327
318
  get resolvedStyle(): IViewStyle {
328
319
  this.cachedResolvedStyle = stableAnchorStyle(