@symbiote-native/angular 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/README.md +13 -10
  2. package/babel-register-composed.cjs +41 -19
  3. package/bin/symbiote-angular-dev.cjs +16 -4
  4. package/build/angular/anchor-host-registry.js +1 -17
  5. package/build/angular/anchor-host-registry.js.map +1 -1
  6. package/build/angular/change-detection-flush.d.ts +31 -0
  7. package/build/angular/change-detection-flush.js +110 -0
  8. package/build/angular/change-detection-flush.js.map +1 -0
  9. package/build/angular/components/activity-indicator-props.d.ts +2 -0
  10. package/build/angular/components/activity-indicator-props.js +2 -0
  11. package/build/angular/components/activity-indicator-props.js.map +1 -0
  12. package/build/angular/components/button-props.d.ts +2 -0
  13. package/build/angular/components/button-props.js +2 -0
  14. package/build/angular/components/button-props.js.map +1 -0
  15. package/build/angular/components/flat-list/index.d.ts +2 -1
  16. package/build/angular/components/flat-list/index.js +22 -10
  17. package/build/angular/components/flat-list/index.js.map +1 -1
  18. package/build/angular/components/image-background-props.d.ts +6 -0
  19. package/build/angular/components/image-background-props.js +2 -0
  20. package/build/angular/components/image-background-props.js.map +1 -0
  21. package/build/angular/components/{image/shared.d.ts → image-shared.d.ts} +1 -1
  22. package/build/angular/components/{image/shared.js → image-shared.js} +7 -2
  23. package/build/angular/components/image-shared.js.map +1 -0
  24. package/build/angular/components/input-accessory-view-props.d.ts +7 -0
  25. package/build/angular/components/input-accessory-view-props.js +9 -0
  26. package/build/angular/components/input-accessory-view-props.js.map +1 -0
  27. package/build/angular/components/keyboard-avoiding-view/index.js +11 -17
  28. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  29. package/build/angular/components/modal/index.d.ts +1 -0
  30. package/build/angular/components/modal/index.js +19 -12
  31. package/build/angular/components/modal/index.js.map +1 -1
  32. package/build/angular/components/pressable-props.d.ts +32 -0
  33. package/build/angular/components/pressable-props.js +11 -0
  34. package/build/angular/components/pressable-props.js.map +1 -0
  35. package/build/angular/components/refresh-control-props.d.ts +16 -0
  36. package/build/angular/components/refresh-control-props.js +14 -0
  37. package/build/angular/components/refresh-control-props.js.map +1 -0
  38. package/build/angular/components/safe-area-view-props.d.ts +7 -0
  39. package/build/angular/components/safe-area-view-props.js +11 -0
  40. package/build/angular/components/safe-area-view-props.js.map +1 -0
  41. package/build/angular/components/scroll-view-props.d.ts +76 -0
  42. package/build/angular/components/scroll-view-props.js +18 -0
  43. package/build/angular/components/scroll-view-props.js.map +1 -0
  44. package/build/angular/components/text-input-props.d.ts +45 -0
  45. package/build/angular/components/text-input-props.js +18 -0
  46. package/build/angular/components/text-input-props.js.map +1 -0
  47. package/build/angular/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +6 -0
  48. package/build/angular/components/touchable-native-feedback/touchable-native-feedback-props.js +2 -0
  49. package/build/angular/components/touchable-native-feedback/touchable-native-feedback-props.js.map +1 -0
  50. package/build/angular/components/touchable-props.d.ts +14 -0
  51. package/build/angular/components/touchable-props.js +13 -0
  52. package/build/angular/components/touchable-props.js.map +1 -0
  53. package/build/angular/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +3 -0
  54. package/build/angular/components/touchable-without-feedback/touchable-without-feedback-props.js +2 -0
  55. package/build/angular/components/touchable-without-feedback/touchable-without-feedback-props.js.map +1 -0
  56. package/build/angular/components/virtualized-list/index.d.ts +5 -1
  57. package/build/angular/components/virtualized-list/index.js +366 -280
  58. package/build/angular/components/virtualized-list/index.js.map +1 -1
  59. package/build/angular/components.d.ts +20 -26
  60. package/build/angular/components.js +12 -14
  61. package/build/angular/components.js.map +1 -1
  62. package/build/angular/create-portal/index.d.ts +2 -2
  63. package/build/angular/create-portal/index.js +5 -5
  64. package/build/angular/create-tunnel/index.js +1 -1
  65. package/build/angular/element-props.d.ts +53 -0
  66. package/build/angular/element-props.js +2 -0
  67. package/build/angular/element-props.js.map +1 -0
  68. package/build/angular/elements.d.ts +528 -0
  69. package/build/angular/elements.js +1444 -0
  70. package/build/angular/elements.js.map +1 -0
  71. package/build/angular/host-instance/index.js +1 -1
  72. package/build/angular/index.d.ts +4 -2
  73. package/build/angular/index.js +15 -1
  74. package/build/angular/index.js.map +1 -1
  75. package/build/angular/modules/animated/create-animated-component.d.ts +5 -6
  76. package/build/angular/modules/animated/create-animated-component.js +60 -73
  77. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  78. package/build/angular/modules/animated/index.js +1 -1
  79. package/build/angular/modules/animated/index.js.map +1 -1
  80. package/build/angular/modules/image/index.d.ts +3 -0
  81. package/build/angular/modules/image/index.js +16 -0
  82. package/build/angular/modules/image/index.js.map +1 -0
  83. package/build/angular/primitives/index.d.ts +18 -18
  84. package/build/angular/primitives/index.js +40 -36
  85. package/build/angular/primitives/index.js.map +1 -1
  86. package/build/angular/primitives/shared.d.ts +2 -3
  87. package/build/angular/primitives/shared.js +8 -53
  88. package/build/angular/primitives/shared.js.map +1 -1
  89. package/build/angular/register.d.ts +1 -1
  90. package/build/angular/register.js +12 -31
  91. package/build/angular/register.js.map +1 -1
  92. package/build/angular/render/index.js +3 -3
  93. package/build/angular/render/index.js.map +1 -1
  94. package/build/angular/renderer/index.js +113 -67
  95. package/build/angular/renderer/index.js.map +1 -1
  96. package/build/angular/renderer/value-change.d.ts +2 -0
  97. package/build/angular/renderer/value-change.js +9 -0
  98. package/build/angular/renderer/value-change.js.map +1 -0
  99. package/metro-config.cjs +23 -5
  100. package/metro-css-parser.cjs +2 -1
  101. package/package.json +5 -14
  102. package/src/anchor-host-registry.ts +1 -17
  103. package/src/change-detection-flush.ts +120 -0
  104. package/src/components/activity-indicator-props.ts +21 -0
  105. package/src/components/button-props.ts +26 -0
  106. package/src/components/flat-list/index.ts +12 -4
  107. package/src/components/image-background-props.ts +24 -0
  108. package/src/components/{image/shared.ts → image-shared.ts} +6 -1
  109. package/src/components/input-accessory-view-props.ts +21 -0
  110. package/src/components/keyboard-avoiding-view/index.ts +6 -9
  111. package/src/components/modal/index.ts +15 -7
  112. package/src/components/pressable-props.ts +73 -0
  113. package/src/components/refresh-control-props.ts +43 -0
  114. package/src/components/safe-area-view-props.ts +28 -0
  115. package/src/components/scroll-view-props.ts +109 -0
  116. package/src/components/text-input-props.ts +72 -0
  117. package/src/components/touchable-native-feedback/touchable-native-feedback-props.ts +23 -0
  118. package/src/components/touchable-props.ts +30 -0
  119. package/src/components/touchable-without-feedback/touchable-without-feedback-props.ts +21 -0
  120. package/src/components/virtualized-list/index.ts +239 -156
  121. package/src/components.ts +84 -35
  122. package/src/create-portal/index.ts +5 -5
  123. package/src/create-tunnel/index.ts +1 -1
  124. package/src/diagnostics.ts +1 -1
  125. package/src/element-props.ts +90 -0
  126. package/src/elements.ts +919 -0
  127. package/src/host-instance/index.ts +1 -1
  128. package/src/index.ts +52 -15
  129. package/src/modules/animated/create-animated-component.ts +50 -55
  130. package/src/modules/animated/index.ts +1 -1
  131. package/src/modules/image/index.ts +18 -0
  132. package/src/primitives/index.ts +22 -18
  133. package/src/primitives/shared.ts +11 -44
  134. package/src/register.ts +12 -40
  135. package/src/render/index.ts +3 -3
  136. package/src/renderer/index.ts +122 -73
  137. package/src/renderer/value-change.ts +8 -0
  138. package/babel-lower-host-primitives.cjs +0 -636
  139. package/build/angular/components/activity-indicator/index.android.d.ts +0 -13
  140. package/build/angular/components/activity-indicator/index.android.js +0 -29
  141. package/build/angular/components/activity-indicator/index.android.js.map +0 -1
  142. package/build/angular/components/activity-indicator/index.d.ts +0 -1
  143. package/build/angular/components/activity-indicator/index.ios.d.ts +0 -9
  144. package/build/angular/components/activity-indicator/index.ios.js +0 -26
  145. package/build/angular/components/activity-indicator/index.ios.js.map +0 -1
  146. package/build/angular/components/activity-indicator/index.js +0 -4
  147. package/build/angular/components/activity-indicator/index.js.map +0 -1
  148. package/build/angular/components/activity-indicator/shared.d.ts +0 -67
  149. package/build/angular/components/activity-indicator/shared.js +0 -260
  150. package/build/angular/components/activity-indicator/shared.js.map +0 -1
  151. package/build/angular/components/button.d.ts +0 -68
  152. package/build/angular/components/button.js +0 -360
  153. package/build/angular/components/button.js.map +0 -1
  154. package/build/angular/components/image/index.android.d.ts +0 -15
  155. package/build/angular/components/image/index.android.js +0 -83
  156. package/build/angular/components/image/index.android.js.map +0 -1
  157. package/build/angular/components/image/index.d.ts +0 -1
  158. package/build/angular/components/image/index.ios.d.ts +0 -15
  159. package/build/angular/components/image/index.ios.js +0 -83
  160. package/build/angular/components/image/index.ios.js.map +0 -1
  161. package/build/angular/components/image/index.js +0 -3
  162. package/build/angular/components/image/index.js.map +0 -1
  163. package/build/angular/components/image/shared.js.map +0 -1
  164. package/build/angular/components/image-background.d.ts +0 -85
  165. package/build/angular/components/image-background.js +0 -443
  166. package/build/angular/components/image-background.js.map +0 -1
  167. package/build/angular/components/input-accessory-view/index.d.ts +0 -66
  168. package/build/angular/components/input-accessory-view/index.js +0 -287
  169. package/build/angular/components/input-accessory-view/index.js.map +0 -1
  170. package/build/angular/components/pressable/index.d.ts +0 -134
  171. package/build/angular/components/pressable/index.js +0 -514
  172. package/build/angular/components/pressable/index.js.map +0 -1
  173. package/build/angular/components/refresh-control/index.d.ts +0 -91
  174. package/build/angular/components/refresh-control/index.js +0 -382
  175. package/build/angular/components/refresh-control/index.js.map +0 -1
  176. package/build/angular/components/safe-area-view/index.d.ts +0 -64
  177. package/build/angular/components/safe-area-view/index.js +0 -287
  178. package/build/angular/components/safe-area-view/index.js.map +0 -1
  179. package/build/angular/components/scroll-view/index.android.d.ts +0 -20
  180. package/build/angular/components/scroll-view/index.android.js +0 -262
  181. package/build/angular/components/scroll-view/index.android.js.map +0 -1
  182. package/build/angular/components/scroll-view/index.d.ts +0 -3
  183. package/build/angular/components/scroll-view/index.ios.d.ts +0 -8
  184. package/build/angular/components/scroll-view/index.ios.js +0 -92
  185. package/build/angular/components/scroll-view/index.ios.js.map +0 -1
  186. package/build/angular/components/scroll-view/index.js +0 -6
  187. package/build/angular/components/scroll-view/index.js.map +0 -1
  188. package/build/angular/components/scroll-view/projection.d.ts +0 -67
  189. package/build/angular/components/scroll-view/projection.js +0 -560
  190. package/build/angular/components/scroll-view/projection.js.map +0 -1
  191. package/build/angular/components/scroll-view/shared.d.ts +0 -241
  192. package/build/angular/components/scroll-view/shared.js +0 -848
  193. package/build/angular/components/scroll-view/shared.js.map +0 -1
  194. package/build/angular/components/scroll-view/sticky-header.d.ts +0 -40
  195. package/build/angular/components/scroll-view/sticky-header.js +0 -224
  196. package/build/angular/components/scroll-view/sticky-header.js.map +0 -1
  197. package/build/angular/components/switch/index.android.d.ts +0 -24
  198. package/build/angular/components/switch/index.android.js +0 -112
  199. package/build/angular/components/switch/index.android.js.map +0 -1
  200. package/build/angular/components/switch/index.d.ts +0 -1
  201. package/build/angular/components/switch/index.ios.d.ts +0 -23
  202. package/build/angular/components/switch/index.ios.js +0 -111
  203. package/build/angular/components/switch/index.ios.js.map +0 -1
  204. package/build/angular/components/switch/index.js +0 -4
  205. package/build/angular/components/switch/index.js.map +0 -1
  206. package/build/angular/components/switch/shared.d.ts +0 -49
  207. package/build/angular/components/switch/shared.js +0 -191
  208. package/build/angular/components/switch/shared.js.map +0 -1
  209. package/build/angular/components/text-input.d.ts +0 -167
  210. package/build/angular/components/text-input.js +0 -684
  211. package/build/angular/components/text-input.js.map +0 -1
  212. package/build/angular/components/touchable/index.d.ts +0 -283
  213. package/build/angular/components/touchable/index.js +0 -1453
  214. package/build/angular/components/touchable/index.js.map +0 -1
  215. package/build/angular/components/touchable-native-feedback/index.d.ts +0 -87
  216. package/build/angular/components/touchable-native-feedback/index.js +0 -409
  217. package/build/angular/components/touchable-native-feedback/index.js.map +0 -1
  218. package/build/angular/state-style.d.ts +0 -9
  219. package/build/angular/state-style.js +0 -39
  220. package/build/angular/state-style.js.map +0 -1
  221. package/metro-transformer.cjs +0 -69
  222. package/src/components/activity-indicator/index.android.ts +0 -23
  223. package/src/components/activity-indicator/index.ios.ts +0 -21
  224. package/src/components/activity-indicator/index.ts +0 -4
  225. package/src/components/activity-indicator/shared.ts +0 -225
  226. package/src/components/button.ts +0 -237
  227. package/src/components/image/index.android.ts +0 -95
  228. package/src/components/image/index.ios.ts +0 -95
  229. package/src/components/image/index.ts +0 -3
  230. package/src/components/image-background.ts +0 -308
  231. package/src/components/input-accessory-view/index.ts +0 -246
  232. package/src/components/pressable/index.ts +0 -522
  233. package/src/components/refresh-control/index.ts +0 -349
  234. package/src/components/safe-area-view/index.ts +0 -247
  235. package/src/components/scroll-view/index.android.ts +0 -205
  236. package/src/components/scroll-view/index.ios.ts +0 -81
  237. package/src/components/scroll-view/index.ts +0 -7
  238. package/src/components/scroll-view/projection.ts +0 -713
  239. package/src/components/scroll-view/shared.ts +0 -1093
  240. package/src/components/scroll-view/sticky-header.ts +0 -267
  241. package/src/components/switch/index.android.ts +0 -113
  242. package/src/components/switch/index.ios.ts +0 -112
  243. package/src/components/switch/index.ts +0 -4
  244. package/src/components/switch/shared.ts +0 -284
  245. package/src/components/text-input.ts +0 -669
  246. package/src/components/touchable/index.ts +0 -956
  247. package/src/components/touchable-native-feedback/index.ts +0 -327
  248. package/src/state-style.ts +0 -39
@@ -9,8 +9,8 @@
9
9
  // in ngAfterViewChecked. OnPush + ChangeDetectorRef.markForCheck() drives re-render off the native
10
10
  // scroll/layout/measure callbacks (they fire outside Angular's own event bindings, so they must
11
11
  // mark the view dirty). This is the Angular twin of the React useReducer/useEffect and Vue
12
- // ref/computed/watch over the same shared functions. It composes the Angular ScrollView, exactly
13
- // as the React/Vue lists drive their ScrollView.
12
+ // ref/computed/watch over the same shared functions. It writes the `scroll-view`/
13
+ // `horizontal-scroll-view` TAG directly, exactly as the React/Vue lists drive their scroll tag.
14
14
  //
15
15
  // Per-item rendering is TEMPLATES, not a callback: React/Vue `renderItem: (info) => element` does
16
16
  // not translate to Angular. The app supplies the cell via `<ng-template vListItem>` (and the
@@ -22,23 +22,27 @@
22
22
  // shape differs, since a callback prop returning an element has no Angular equivalent.
23
23
  //
24
24
  // Lists have no Descriptor render fn — the cell content is the framework's own children.
25
- // Cells/spacers are plain symbiote-view host elements.
25
+ // Cells/spacers are plain view host elements.
26
26
  import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, Input, Output, ViewChild, computed, inject, signal, } from '@angular/core';
27
- import { DEFAULT_END_REACHED_THRESHOLD, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_START_REACHED_THRESHOLD, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, buildListPlan, buildViewabilityPairs, computeWindow, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readLayoutOffset, readScrollOffset, reduceList, resolveAccessibilityProps, resolveItemKey, } from '@symbiote-native/components';
28
- import { dlog, flattenStyle, } from '@symbiote-native/engine';
27
+ import { DEFAULT_END_REACHED_THRESHOLD, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_START_REACHED_THRESHOLD, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, buildListPlan, buildViewabilityPairs, computeWindow, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readLayoutOffset, readScrollOffset, reduceList, resolveAccessibilityProps, resolveItemKey, buildScrollViewHandle, } from '@symbiote-native/components';
28
+ import { dlog, flattenStyle, isSymbioteNode, } from '@symbiote-native/engine';
29
29
  import { countAngular } from '../../diagnostics';
30
- import { ScrollView } from '../scroll-view';
31
- import { RefreshControl } from '../refresh-control';
32
- import { anchorHostStyle, SymbioteStyleInputDirective, ViewHost, } from '../../primitives';
30
+ import { anchorHostStyle, SymbioteHostPropsDirective, SymbioteStyleInputDirective, ViewHost, } from '../../primitives';
33
31
  import { VListEmptyDirective, VListFooterDirective, VListHeaderDirective, VListItemDirective, VListOutletDirective, VListSeparatorDirective, } from './directives';
34
- import { gateWanted, injectGateDemandAbove, provideGateDemand, } from '../../gate-demand';
32
+ import { gateWanted, injectGateDemandAbove, } from '../../gate-demand';
35
33
  import * as i0 from "@angular/core";
36
34
  import * as i1 from "../../primitives/shared";
37
35
  export { VListEmptyDirective, VListFooterDirective, VListHeaderDirective, VListItemDirective, VListSeparatorDirective, } from './directives';
36
+ // The scroll body is identical on both axes; only the outer tag differs (the axis comes from the
37
+ // TAG, never a prop — `<horizontal-scroll-view>` vs `<scroll-view>`, matching every other adapter,
38
+ // `.claude/rules/host-primitive-tier.md`). No shared-outlet primitive exists to de-duplicate this
39
+ // (this package's own NgTemplateOutlet twin is keyed on a per-cell context, not a static block, and
40
+ // ngc's partial evaluator needs the decorator's `template` to be one literal), so the body is
41
+ // duplicated across the two branches — the same shape Svelte's own `{#if horizontal}` takes.
38
42
  export class VirtualizedList {
39
43
  // The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
40
44
  // `[onEndReached]="…"`. See handleRefresh/accessibility*Tick below for how the still-callback-
41
- // shaped ScrollView/RefreshControl @Input()s are fed from these.
45
+ // shaped scroll-view/refresh-control bag keys are fed from these.
42
46
  endReached = new EventEmitter();
43
47
  startReached = new EventEmitter();
44
48
  refresh = new EventEmitter();
@@ -137,10 +141,19 @@ export class VirtualizedList {
137
141
  footerDir;
138
142
  emptyDir;
139
143
  separatorDir;
140
- // The composed inner scroll view. Its instance IS an IScrollViewHandle (scrollTo / scrollToEnd /
141
- // flashScrollIndicators / getScrollNode), so the imperative handle delegates straight to it.
142
- // Available from ngAfterViewInit on; the handle reads the node lazily and no-ops before commit.
143
- scrollView;
144
+ // The inner scroll TAG's own engine node a template ref on a bare intrinsic hands back the
145
+ // host node directly (`isSymbioteNode(elementRef.nativeElement)`, matching `anchorHostStyle`'s
146
+ // own reader below), so there is no wrapper component left to read a handle off. Present from
147
+ // whichever branch (`scroll-view` / `horizontal-scroll-view`) is currently mounted, once the
148
+ // template has run at least one CD pass.
149
+ scrollHostRef;
150
+ get scrollNode() {
151
+ const node = this.scrollHostRef?.nativeElement;
152
+ return isSymbioteNode(node) ? node : null;
153
+ }
154
+ // `buildScrollViewHandle` reads the node lazily through the getter above and no-ops before
155
+ // commit — the same function every other adapter feeds from its own host-instance accessor.
156
+ scrollHandle = buildScrollViewHandle(() => this.scrollNode);
144
157
  // --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
145
158
  itemCount = EMPTY_OFFSET;
146
159
  windowCells = [];
@@ -209,12 +222,12 @@ export class VirtualizedList {
209
222
  lastRecompute = undefined;
210
223
  cdr = inject(ChangeDetectorRef);
211
224
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
212
- // onto (see anchorHostStyle's doc comment) — NOT `scrollView` above, which targets the real
213
- // inner `<ScrollView>` one level down (itself its own separate anchor host).
225
+ // onto (see anchorHostStyle's doc comment) — NOT `scrollHostRef` above, which targets the real
226
+ // inner `<scroll-view>`/`<horizontal-scroll-view>` tag one level down.
214
227
  elementRef = inject(ElementRef);
215
- // Bridges the non-reactive @Input fields into the reactive graph so the computed below can
216
- // memoize a bag derived from them (the same bridge ScrollView's shared.ts uses - read its comment
217
- // for why signal inputs are not an option while this package's unit suite runs on JIT).
228
+ // Bridges the non-reactive @Input fields into the reactive graph so `foldedAccessibility` below
229
+ // can memoize a bag derived from them (read its own comment for why signal inputs are not an
230
+ // option while this package's unit suite runs on JIT).
218
231
  //
219
232
  // ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
220
233
  // (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
@@ -223,12 +236,13 @@ export class VirtualizedList {
223
236
  // instead of widening this one.
224
237
  inputsRevision = signal(0, /* @ts-ignore */
225
238
  ...(ngDevMode ? [{ debugName: "inputsRevision" }] : /* istanbul ignore next */ []));
226
- // MEASURED: the template feeds 20 separate <ScrollView> inputs off this one bag, and Angular does
227
- // not cache a getter across binding expressions - as a getter it rebuilt the object 20 times per
228
- // refresh, and every consumer saw a fresh reference, so all 20 reported "changed" and wrote
229
- // through to the ScrollView. A computed evaluates once and hands back the SAME object until an
230
- // input actually changes. Every dependency below is an @Input, which is what makes
231
- // inputsRevision a complete dependency set here.
239
+ // MEASURED (while ScrollView was still a wrapper): the template fed 20 separate inputs off this
240
+ // one bag, and Angular does not cache a getter across binding expressions - as a getter it
241
+ // rebuilt the object 20 times per refresh, and every consumer saw a fresh reference, so all 20
242
+ // reported "changed" and wrote through. A computed evaluates once and hands back the SAME object
243
+ // until an input actually changes. Every dependency below is an @Input, which is what makes
244
+ // inputsRevision a complete dependency set here — `scrollViewBag()` reuses this result rather
245
+ // than re-running the fold.
232
246
  foldedAccessibility = computed(() => {
233
247
  this.inputsRevision();
234
248
  return resolveAccessibilityProps({
@@ -273,6 +287,54 @@ export class VirtualizedList {
273
287
  get shouldRenderRefreshControl() {
274
288
  return this.refreshRequested ?? this.refresh.observed;
275
289
  }
290
+ // The flat bag fed to the scroll tag via `[symbioteHostProps]`. A plain method, not `computed()`:
291
+ // most of these fields (resolvedStyle, renderedStickyIndices, commandedOffset, …) are recomputed
292
+ // in ngDoCheck/dispatch as ordinary fields, not signals, so a computed() memoized only against
293
+ // `inputsRevision` would go stale the moment one of them changed outside an @Input() write. The
294
+ // directive itself diffs per key before writing to the engine (`primitives/shared.ts`), so a
295
+ // fresh object every CD pass costs nothing beyond the literal allocation.
296
+ scrollViewBag() {
297
+ return {
298
+ ...this.foldedAccessibility(),
299
+ style: this.resolvedStyle,
300
+ contentContainerStyle: this.resolvedContentContainerStyle,
301
+ onScroll: this.onScrollTick,
302
+ onLayout: this.onLayoutTick,
303
+ onScrollBeginDrag: this.onScrollBeginDrag,
304
+ onScrollEndDrag: this.onScrollEndDrag,
305
+ onMomentumScrollBegin: this.onMomentumScrollBegin,
306
+ onMomentumScrollEnd: this.onMomentumScrollEnd,
307
+ scrollEventThrottle: this.scrollEventThrottle,
308
+ keyboardShouldPersistTaps: this.keyboardShouldPersistTaps,
309
+ keyboardDismissMode: this.keyboardDismissMode,
310
+ contentOffset: this.commandedOffset,
311
+ stickyHeaderIndices: this.renderedStickyIndices,
312
+ maintainVisibleContentPosition: this.resolvedMaintainVisibleContentPosition,
313
+ // The four boolean-gated Fabric events (`.claude/rules/fabric-boolean-event-gates.md`): the
314
+ // TAG has no `.observed` of its own, so `wantsGate` (own subscriber, or the demand from
315
+ // whatever wraps this list) is what decides whether the flag reaches Fabric at all.
316
+ onAccessibilityAction: this.wantsGate('accessibilityAction')
317
+ ? this.accessibilityActionTick
318
+ : undefined,
319
+ onAccessibilityTap: this.wantsGate('accessibilityTap')
320
+ ? this.accessibilityTapTick
321
+ : undefined,
322
+ onMagicTap: this.wantsGate('magicTap') ? this.magicTapTick : undefined,
323
+ onAccessibilityEscape: this.wantsGate('accessibilityEscape')
324
+ ? this.accessibilityEscapeTick
325
+ : undefined,
326
+ };
327
+ }
328
+ // The `refresh-control` tag's own bag — an ordinary first child, re-parented per platform by the
329
+ // scroll-view behavior (`../refresh-control-props.ts`). `onRefresh` is a plain listener prop, not
330
+ // a gated one: RefreshControl's `refresh` is unaffected by `fabric-boolean-event-gates.md`.
331
+ refreshControlBag() {
332
+ return {
333
+ refreshing: this.refreshing ?? false,
334
+ progressViewOffset: this.progressViewOffset,
335
+ onRefresh: this.handleRefresh,
336
+ };
337
+ }
276
338
  get isHorizontal() {
277
339
  return this.horizontal === true;
278
340
  }
@@ -297,9 +359,10 @@ export class VirtualizedList {
297
359
  get onStartReachedThresholdValue() {
298
360
  return this.onStartReachedThreshold ?? DEFAULT_START_REACHED_THRESHOLD;
299
361
  }
300
- // ScrollView's onScroll stays an @Input() callback (an Animated.event(...) target must be able to
301
- // flow through it, which an @Output() can't carry), so this is a stable arrow field passed
302
- // straight to [onScroll]. onLayout is a real @Output() now, bound via (layout)="onLayoutTick($event)".
362
+ // onScroll stays a plain callback bag key (an Animated.event(...) target must be able to flow
363
+ // through it, which an @Output() can't carry), so this is a stable arrow field read straight into
364
+ // scrollViewBag()'s `onScroll`. onLayout is unconditional too this list always needs its own
365
+ // layout for windowing, regardless of whether an app subscribes to anything.
303
366
  onScrollTick = (event) => {
304
367
  countAngular('scrollTicks');
305
368
  const offset = readScrollOffset(event, this.isHorizontal);
@@ -319,9 +382,9 @@ export class VirtualizedList {
319
382
  dlog(`Angular VirtualizedList onLayout viewport=${length}`);
320
383
  this.dispatch({ kind: 'layout', length });
321
384
  };
322
- // RefreshControl's refresh and ScrollView's accessibility events are real @Output()s now, bound
323
- // via (refresh)="handleRefresh()" / (accessibilityAction)="accessibilityActionTick($event)" etc.
324
- // — these fields just adapt VirtualizedList's own @Output() into a plain re-emit callback.
385
+ // RefreshControl's refresh and the scroll tag's accessibility events reach here as plain
386
+ // listener-prop values in scrollViewBag()/refreshControlBag() — these fields just adapt
387
+ // VirtualizedList's own @Output() into a plain re-emit callback.
325
388
  handleRefresh = () => {
326
389
  this.refresh.emit();
327
390
  };
@@ -789,19 +852,19 @@ export class VirtualizedList {
789
852
  });
790
853
  }
791
854
  flashScrollIndicators() {
792
- this.scrollView?.flashScrollIndicators();
855
+ this.scrollHandle.flashScrollIndicators();
793
856
  }
794
857
  getNativeScrollRef() {
795
- return this.scrollView ?? null;
858
+ return this.scrollNode !== null ? this.scrollHandle : null;
796
859
  }
797
860
  getScrollableNode() {
798
- return this.scrollView ?? null;
861
+ return this.scrollNode !== null ? this.scrollHandle : null;
799
862
  }
800
863
  getScrollResponder() {
801
- return this.scrollView ?? null;
864
+ return this.scrollNode !== null ? this.scrollHandle : null;
802
865
  }
803
866
  getScrollNode() {
804
- return this.scrollView?.getScrollNode() ?? null;
867
+ return this.scrollHandle.getScrollNode();
805
868
  }
806
869
  recordInteraction() {
807
870
  this.dispatch({ kind: 'record-interaction' });
@@ -811,9 +874,9 @@ export class VirtualizedList {
811
874
  const target = this.isHorizontal
812
875
  ? { x: clamped, y: EMPTY_OFFSET }
813
876
  : { x: EMPTY_OFFSET, y: clamped };
814
- if (this.scrollView !== undefined) {
877
+ if (this.scrollNode !== null) {
815
878
  dlog(`Angular VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${this.isHorizontal})`);
816
- this.scrollView.scrollTo({ x: target.x, y: target.y, animated });
879
+ this.scrollHandle.scrollTo({ x: target.x, y: target.y, animated });
817
880
  return;
818
881
  }
819
882
  dlog(`Angular VirtualizedList scrollTo offset=${clamped} pending-ref`);
@@ -821,278 +884,301 @@ export class VirtualizedList {
821
884
  this.cdr.markForCheck();
822
885
  }
823
886
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VirtualizedList, deps: [], target: i0.ɵɵFactoryTarget.Component });
824
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: VirtualizedList, isStandalone: true, selector: "VirtualizedList", inputs: { data: "data", getItem: "getItem", getItemCount: "getItemCount", keyExtractor: "keyExtractor", getItemLayout: "getItemLayout", horizontal: "horizontal", inverted: "inverted", extraData: "extraData", onEndReachedThreshold: "onEndReachedThreshold", onStartReachedThreshold: "onStartReachedThreshold", refreshing: "refreshing", progressViewOffset: "progressViewOffset", refreshRequested: "refreshRequested", viewabilityConfig: "viewabilityConfig", viewabilityConfigCallbackPairs: "viewabilityConfigCallbackPairs", initialNumToRender: "initialNumToRender", initialScrollIndex: "initialScrollIndex", maxToRenderPerBatch: "maxToRenderPerBatch", updateCellsBatchingPeriod: "updateCellsBatchingPeriod", windowSize: "windowSize", stickyHeaderIndices: "stickyHeaderIndices", maintainVisibleContentPosition: "maintainVisibleContentPosition", onScroll: "onScroll", onScrollBeginDrag: "onScrollBeginDrag", onScrollEndDrag: "onScrollEndDrag", onMomentumScrollBegin: "onMomentumScrollBegin", onMomentumScrollEnd: "onMomentumScrollEnd", scrollEventThrottle: "scrollEventThrottle", keyboardShouldPersistTaps: "keyboardShouldPersistTaps", keyboardDismissMode: "keyboardDismissMode", style: "style", contentContainerStyle: "contentContainerStyle", testID: "testID", nativeID: "nativeID", accessible: "accessible", accessibilityLabel: "accessibilityLabel", accessibilityHint: "accessibilityHint", accessibilityRole: "accessibilityRole", accessibilityState: "accessibilityState", accessibilityValue: "accessibilityValue", accessibilityActions: "accessibilityActions", accessibilityLabelledBy: "accessibilityLabelledBy", importantForAccessibility: "importantForAccessibility", accessibilityLiveRegion: "accessibilityLiveRegion", screenReaderFocusable: "screenReaderFocusable", accessibilityViewIsModal: "accessibilityViewIsModal", accessibilityElementsHidden: "accessibilityElementsHidden", accessibilityIgnoresInvertColors: "accessibilityIgnoresInvertColors", accessibilityLanguage: "accessibilityLanguage", accessibilityRespondsToUserInteraction: "accessibilityRespondsToUserInteraction", accessibilityShowsLargeContentViewer: "accessibilityShowsLargeContentViewer", accessibilityLargeContentTitle: "accessibilityLargeContentTitle", role: "role", ariaLabel: "ariaLabel", ariaLabelledBy: "ariaLabelledBy", ariaLive: "ariaLive", ariaHidden: "ariaHidden", ariaBusy: "ariaBusy", ariaChecked: "ariaChecked", ariaDisabled: "ariaDisabled", ariaExpanded: "ariaExpanded", ariaSelected: "ariaSelected", ariaModal: "ariaModal", ariaValueMax: "ariaValueMax", ariaValueMin: "ariaValueMin", ariaValueNow: "ariaValueNow", ariaValueText: "ariaValueText" }, outputs: { endReached: "endReached", startReached: "startReached", refresh: "refresh", viewableItemsChanged: "viewableItemsChanged", scrollToIndexFailed: "scrollToIndexFailed", accessibilityAction: "accessibilityAction", accessibilityTap: "accessibilityTap", magicTap: "magicTap", accessibilityEscape: "accessibilityEscape" }, queries: [{ propertyName: "itemDir", first: true, predicate: VListItemDirective, descendants: true }, { propertyName: "headerDir", first: true, predicate: VListHeaderDirective, descendants: true }, { propertyName: "footerDir", first: true, predicate: VListFooterDirective, descendants: true }, { propertyName: "emptyDir", first: true, predicate: VListEmptyDirective, descendants: true }, { propertyName: "separatorDir", first: true, predicate: VListSeparatorDirective, descendants: true }], viewQueries: [{ propertyName: "scrollView", first: true, predicate: ScrollView, descendants: true }], usesOnChanges: true, hostDirectives: [{ directive: i1.SymbioteStyleInputDirective, inputs: ["style", "style"] }], ngImport: i0, template: `
825
- <ScrollView
826
- [horizontal]="isHorizontal"
827
- [style]="resolvedStyle"
828
- [contentContainerStyle]="resolvedContentContainerStyle"
829
- [testID]="foldedAccessibility().testID"
830
- [nativeID]="foldedAccessibility().nativeID"
831
- [accessible]="foldedAccessibility().accessible"
832
- [accessibilityLabel]="foldedAccessibility().accessibilityLabel"
833
- [accessibilityHint]="foldedAccessibility().accessibilityHint"
834
- [accessibilityRole]="foldedAccessibility().accessibilityRole"
835
- [accessibilityState]="foldedAccessibility().accessibilityState"
836
- [accessibilityValue]="foldedAccessibility().accessibilityValue"
837
- [accessibilityActions]="foldedAccessibility().accessibilityActions"
838
- [accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
839
- [importantForAccessibility]="
840
- foldedAccessibility().importantForAccessibility
841
- "
842
- [accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
843
- [screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
844
- [accessibilityViewIsModal]="
845
- foldedAccessibility().accessibilityViewIsModal
846
- "
847
- [accessibilityElementsHidden]="
848
- foldedAccessibility().accessibilityElementsHidden
849
- "
850
- [accessibilityIgnoresInvertColors]="
851
- foldedAccessibility().accessibilityIgnoresInvertColors
852
- "
853
- [accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
854
- [accessibilityRespondsToUserInteraction]="
855
- foldedAccessibility().accessibilityRespondsToUserInteraction
856
- "
857
- [accessibilityShowsLargeContentViewer]="
858
- foldedAccessibility().accessibilityShowsLargeContentViewer
859
- "
860
- [accessibilityLargeContentTitle]="
861
- foldedAccessibility().accessibilityLargeContentTitle
862
- "
863
- (accessibilityAction)="accessibilityActionTick($event)"
864
- (accessibilityTap)="accessibilityTapTick($event)"
865
- (magicTap)="magicTapTick($event)"
866
- (accessibilityEscape)="accessibilityEscapeTick($event)"
867
- [onScroll]="onScrollTick"
868
- (layout)="onLayoutTick($event)"
869
- [onScrollBeginDrag]="onScrollBeginDrag"
870
- [onScrollEndDrag]="onScrollEndDrag"
871
- [onMomentumScrollBegin]="onMomentumScrollBegin"
872
- [onMomentumScrollEnd]="onMomentumScrollEnd"
873
- [scrollEventThrottle]="scrollEventThrottle"
874
- [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
875
- [keyboardDismissMode]="keyboardDismissMode"
876
- [contentOffset]="commandedOffset"
877
- [stickyHeaderIndices]="renderedStickyIndices"
878
- [maintainVisibleContentPosition]="resolvedMaintainVisibleContentPosition"
879
- >
880
- @if (shouldRenderRefreshControl) {
881
- <RefreshControl
882
- [refreshing]="refreshing ?? false"
883
- (refresh)="handleRefresh()"
884
- [progressViewOffset]="progressViewOffset"
885
- />
886
- }
887
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: VirtualizedList, isStandalone: true, selector: "VirtualizedList", inputs: { data: "data", getItem: "getItem", getItemCount: "getItemCount", keyExtractor: "keyExtractor", getItemLayout: "getItemLayout", horizontal: "horizontal", inverted: "inverted", extraData: "extraData", onEndReachedThreshold: "onEndReachedThreshold", onStartReachedThreshold: "onStartReachedThreshold", refreshing: "refreshing", progressViewOffset: "progressViewOffset", refreshRequested: "refreshRequested", viewabilityConfig: "viewabilityConfig", viewabilityConfigCallbackPairs: "viewabilityConfigCallbackPairs", initialNumToRender: "initialNumToRender", initialScrollIndex: "initialScrollIndex", maxToRenderPerBatch: "maxToRenderPerBatch", updateCellsBatchingPeriod: "updateCellsBatchingPeriod", windowSize: "windowSize", stickyHeaderIndices: "stickyHeaderIndices", maintainVisibleContentPosition: "maintainVisibleContentPosition", onScroll: "onScroll", onScrollBeginDrag: "onScrollBeginDrag", onScrollEndDrag: "onScrollEndDrag", onMomentumScrollBegin: "onMomentumScrollBegin", onMomentumScrollEnd: "onMomentumScrollEnd", scrollEventThrottle: "scrollEventThrottle", keyboardShouldPersistTaps: "keyboardShouldPersistTaps", keyboardDismissMode: "keyboardDismissMode", style: "style", contentContainerStyle: "contentContainerStyle", testID: "testID", nativeID: "nativeID", accessible: "accessible", accessibilityLabel: "accessibilityLabel", accessibilityHint: "accessibilityHint", accessibilityRole: "accessibilityRole", accessibilityState: "accessibilityState", accessibilityValue: "accessibilityValue", accessibilityActions: "accessibilityActions", accessibilityLabelledBy: "accessibilityLabelledBy", importantForAccessibility: "importantForAccessibility", accessibilityLiveRegion: "accessibilityLiveRegion", screenReaderFocusable: "screenReaderFocusable", accessibilityViewIsModal: "accessibilityViewIsModal", accessibilityElementsHidden: "accessibilityElementsHidden", accessibilityIgnoresInvertColors: "accessibilityIgnoresInvertColors", accessibilityLanguage: "accessibilityLanguage", accessibilityRespondsToUserInteraction: "accessibilityRespondsToUserInteraction", accessibilityShowsLargeContentViewer: "accessibilityShowsLargeContentViewer", accessibilityLargeContentTitle: "accessibilityLargeContentTitle", role: "role", ariaLabel: "ariaLabel", ariaLabelledBy: "ariaLabelledBy", ariaLive: "ariaLive", ariaHidden: "ariaHidden", ariaBusy: "ariaBusy", ariaChecked: "ariaChecked", ariaDisabled: "ariaDisabled", ariaExpanded: "ariaExpanded", ariaSelected: "ariaSelected", ariaModal: "ariaModal", ariaValueMax: "ariaValueMax", ariaValueMin: "ariaValueMin", ariaValueNow: "ariaValueNow", ariaValueText: "ariaValueText" }, outputs: { endReached: "endReached", startReached: "startReached", refresh: "refresh", viewableItemsChanged: "viewableItemsChanged", scrollToIndexFailed: "scrollToIndexFailed", accessibilityAction: "accessibilityAction", accessibilityTap: "accessibilityTap", magicTap: "magicTap", accessibilityEscape: "accessibilityEscape" }, queries: [{ propertyName: "itemDir", first: true, predicate: VListItemDirective, descendants: true }, { propertyName: "headerDir", first: true, predicate: VListHeaderDirective, descendants: true }, { propertyName: "footerDir", first: true, predicate: VListFooterDirective, descendants: true }, { propertyName: "emptyDir", first: true, predicate: VListEmptyDirective, descendants: true }, { propertyName: "separatorDir", first: true, predicate: VListSeparatorDirective, descendants: true }], viewQueries: [{ propertyName: "scrollHostRef", first: true, predicate: ["scrollHost"], descendants: true, read: ElementRef }], usesOnChanges: true, hostDirectives: [{ directive: i1.SymbioteStyleInputDirective, inputs: ["style", "style"] }], ngImport: i0, template: `
888
+ @if (isHorizontal) {
889
+ <horizontal-scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
890
+ @if (shouldRenderRefreshControl) {
891
+ <refresh-control
892
+ [symbioteHostProps]="refreshControlBag()"
893
+ ></refresh-control>
894
+ }
887
895
 
888
- @if (headerDir !== undefined) {
889
- <symbiote-view>
890
- <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
891
- </symbiote-view>
892
- }
896
+ @if (headerDir !== undefined) {
897
+ <view>
898
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
899
+ </view>
900
+ }
893
901
 
894
- @if (itemCount === 0) {
895
- @if (emptyDir !== undefined) {
896
- <symbiote-view>
897
- <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
898
- </symbiote-view>
902
+ @if (itemCount === 0) {
903
+ @if (emptyDir !== undefined) {
904
+ <view>
905
+ <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
906
+ </view>
907
+ }
908
+ } @else {
909
+ @if (leadingSpacerStyle !== null) {
910
+ <view [style]="leadingSpacerStyle"></view>
911
+ }
912
+ @if (forcedStickyCell !== null) {
913
+ <view
914
+ (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
915
+ [style]="cellStyle"
916
+ >
917
+ <ng-container
918
+ [vListOutlet]="itemDir?.templateRef"
919
+ [vListOutletContext]="forcedStickyCell.context"
920
+ ></ng-container>
921
+ </view>
922
+ }
923
+ @if (gapSpacerStyle !== null) {
924
+ <view [style]="gapSpacerStyle"></view>
925
+ }
926
+ @for (cell of windowCells; track cell.key) {
927
+ <view
928
+ (layout)="handleCellLayout(cell.measure, $event)"
929
+ [style]="cellStyle"
930
+ >
931
+ <ng-container
932
+ [vListOutlet]="itemDir?.templateRef"
933
+ [vListOutletContext]="cell.context"
934
+ ></ng-container>
935
+ @if (cell.separatorContext !== undefined) {
936
+ <ng-container
937
+ [vListOutlet]="separatorDir?.templateRef"
938
+ [vListOutletContext]="cell.separatorContext"
939
+ ></ng-container>
940
+ }
941
+ </view>
942
+ }
943
+ @if (trailingSpacerStyle !== null) {
944
+ <view [style]="trailingSpacerStyle"></view>
945
+ }
899
946
  }
900
- } @else {
901
- @if (leadingSpacerStyle !== null) {
902
- <symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
947
+
948
+ @if (footerDir !== undefined) {
949
+ <view>
950
+ <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
951
+ </view>
903
952
  }
904
- @if (forcedStickyCell !== null) {
905
- <symbiote-view
906
- (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
907
- [style]="cellStyle"
908
- >
909
- <ng-container
910
- [vListOutlet]="itemDir?.templateRef"
911
- [vListOutletContext]="forcedStickyCell.context"
912
- ></ng-container>
913
- </symbiote-view>
953
+ </horizontal-scroll-view>
954
+ } @else {
955
+ <scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
956
+ @if (shouldRenderRefreshControl) {
957
+ <refresh-control
958
+ [symbioteHostProps]="refreshControlBag()"
959
+ ></refresh-control>
914
960
  }
915
- @if (gapSpacerStyle !== null) {
916
- <symbiote-view [style]="gapSpacerStyle"></symbiote-view>
961
+
962
+ @if (headerDir !== undefined) {
963
+ <view>
964
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
965
+ </view>
917
966
  }
918
- <!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
919
- (VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
920
- the chrome between two cells is gap + separator + gap while a spacer collapsing that
921
- region replaces it with a single gap — every cell below the leading spacer then lands
922
- short by (separator + gap) and the content jumps by that much whenever the window's
923
- first index moves. Measured at exactly 17px on device 2026-08-19; see
924
- .claude/rules/list-geometry-feedback-loop.md. -->
925
- @for (cell of windowCells; track cell.key) {
926
- <symbiote-view
927
- (layout)="handleCellLayout(cell.measure, $event)"
928
- [style]="cellStyle"
929
- >
930
- <ng-container
931
- [vListOutlet]="itemDir?.templateRef"
932
- [vListOutletContext]="cell.context"
933
- ></ng-container>
934
- @if (cell.separatorContext !== undefined) {
967
+
968
+ @if (itemCount === 0) {
969
+ @if (emptyDir !== undefined) {
970
+ <view>
971
+ <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
972
+ </view>
973
+ }
974
+ } @else {
975
+ @if (leadingSpacerStyle !== null) {
976
+ <view [style]="leadingSpacerStyle"></view>
977
+ }
978
+ @if (forcedStickyCell !== null) {
979
+ <view
980
+ (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
981
+ [style]="cellStyle"
982
+ >
935
983
  <ng-container
936
- [vListOutlet]="separatorDir?.templateRef"
937
- [vListOutletContext]="cell.separatorContext"
984
+ [vListOutlet]="itemDir?.templateRef"
985
+ [vListOutletContext]="forcedStickyCell.context"
938
986
  ></ng-container>
939
- }
940
- </symbiote-view>
941
- }
942
- @if (trailingSpacerStyle !== null) {
943
- <symbiote-view [style]="trailingSpacerStyle"></symbiote-view>
987
+ </view>
988
+ }
989
+ @if (gapSpacerStyle !== null) {
990
+ <view [style]="gapSpacerStyle"></view>
991
+ }
992
+ <!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
993
+ (VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
994
+ the chrome between two cells is gap + separator + gap while a spacer collapsing that
995
+ region replaces it with a single gap — every cell below the leading spacer then lands
996
+ short by (separator + gap) and the content jumps by that much whenever the window's
997
+ first index moves. Measured at exactly 17px on device 2026-08-19; see
998
+ .claude/rules/list-geometry-feedback-loop.md. -->
999
+ @for (cell of windowCells; track cell.key) {
1000
+ <view
1001
+ (layout)="handleCellLayout(cell.measure, $event)"
1002
+ [style]="cellStyle"
1003
+ >
1004
+ <ng-container
1005
+ [vListOutlet]="itemDir?.templateRef"
1006
+ [vListOutletContext]="cell.context"
1007
+ ></ng-container>
1008
+ @if (cell.separatorContext !== undefined) {
1009
+ <ng-container
1010
+ [vListOutlet]="separatorDir?.templateRef"
1011
+ [vListOutletContext]="cell.separatorContext"
1012
+ ></ng-container>
1013
+ }
1014
+ </view>
1015
+ }
1016
+ @if (trailingSpacerStyle !== null) {
1017
+ <view [style]="trailingSpacerStyle"></view>
1018
+ }
944
1019
  }
945
- }
946
1020
 
947
- @if (footerDir !== undefined) {
948
- <symbiote-view>
949
- <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
950
- </symbiote-view>
951
- }
952
- </ScrollView>
953
- `, isInline: true, dependencies: [{ kind: "component", type: ScrollView, selector: "ScrollView", inputs: ["style", "contentContainerStyle", "horizontal", "scrollEnabled", "showsVerticalScrollIndicator", "showsHorizontalScrollIndicator", "pagingEnabled", "bounces", "decelerationRate", "scrollEventThrottle", "contentInset", "contentOffset", "removeClippedSubviews", "snapToInterval", "snapToOffsets", "snapToAlignment", "snapToStart", "snapToEnd", "disableIntervalMomentum", "stickyHeaderIndices", "invertStickyHeaders", "StickyHeaderComponent", "keyboardDismissMode", "keyboardShouldPersistTaps", "maintainVisibleContentPosition", "alwaysBounceHorizontal", "alwaysBounceVertical", "centerContent", "scrollIndicatorInsets", "indicatorStyle", "directionalLockEnabled", "automaticallyAdjustKeyboardInsets", "contentInsetAdjustmentBehavior", "minimumZoomScale", "maximumZoomScale", "zoomScale", "bouncesZoom", "pinchGestureEnabled", "nestedScrollEnabled", "overScrollMode", "fadingEdgeLength", "persistentScrollbar", "endFillColor", "onScroll", "onScrollBeginDrag", "onScrollEndDrag", "onMomentumScrollBegin", "onMomentumScrollEnd", "testID", "nativeID", "accessible", "accessibilityLabel", "accessibilityHint", "accessibilityRole", "accessibilityState", "accessibilityValue", "accessibilityActions", "accessibilityLabelledBy", "importantForAccessibility", "accessibilityLiveRegion", "screenReaderFocusable", "accessibilityViewIsModal", "accessibilityElementsHidden", "accessibilityIgnoresInvertColors", "accessibilityLanguage", "accessibilityRespondsToUserInteraction", "accessibilityShowsLargeContentViewer", "accessibilityLargeContentTitle", "role", "aria-label", "aria-labelledby", "aria-live", "aria-hidden", "aria-busy", "aria-checked", "aria-disabled", "aria-expanded", "aria-selected", "aria-modal", "aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-valuetext"] }, { kind: "component", type: RefreshControl, selector: "RefreshControl", inputs: ["refreshing", "tintColor", "title", "titleColor", "progressViewOffset", "colors", "progressBackgroundColor", "size", "enabled", "style", "testID", "nativeID", "accessible", "accessibilityLabel", "accessibilityHint", "accessibilityRole", "accessibilityState", "accessibilityValue", "accessibilityActions", "accessibilityLabelledBy", "importantForAccessibility", "accessibilityLiveRegion", "screenReaderFocusable", "accessibilityViewIsModal", "accessibilityElementsHidden", "accessibilityIgnoresInvertColors", "accessibilityLanguage", "accessibilityRespondsToUserInteraction", "accessibilityShowsLargeContentViewer", "accessibilityLargeContentTitle", "role", "aria-label", "aria-labelledby", "aria-live", "aria-hidden", "aria-busy", "aria-checked", "aria-disabled", "aria-expanded", "aria-selected", "aria-modal", "aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-valuetext"], outputs: ["refresh", "accessibilityAction", "accessibilityTap", "magicTap", "accessibilityEscape"] }, { kind: "directive", type: VListOutletDirective, selector: "[vListOutlet]", inputs: ["vListOutlet", "vListOutletContext"] }, { kind: "component", type: ViewHost, selector: "symbiote-view, View" }], viewProviders: [provideGateDemand(() => VirtualizedList)], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1021
+ @if (footerDir !== undefined) {
1022
+ <view>
1023
+ <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
1024
+ </view>
1025
+ }
1026
+ </scroll-view>
1027
+ }
1028
+ `, isInline: true, dependencies: [{ kind: "directive", type: SymbioteHostPropsDirective, selector: "[symbioteHostProps]", inputs: ["symbioteHostProps"], exportAs: ["symbioteHost"] }, { kind: "directive", type: VListOutletDirective, selector: "[vListOutlet]", inputs: ["vListOutlet", "vListOutletContext"] }, { kind: "component", type: ViewHost, selector: "view" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
954
1029
  }
955
1030
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VirtualizedList, decorators: [{
956
1031
  type: Component,
957
1032
  args: [{
958
1033
  selector: 'VirtualizedList',
959
1034
  standalone: true,
960
- viewProviders: [provideGateDemand(() => VirtualizedList)],
961
1035
  hostDirectives: [
962
1036
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
963
1037
  ],
964
1038
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
965
- imports: [ScrollView, RefreshControl, VListOutletDirective, ViewHost],
1039
+ imports: [SymbioteHostPropsDirective, VListOutletDirective, ViewHost],
966
1040
  changeDetection: ChangeDetectionStrategy.OnPush,
967
1041
  template: `
968
- <ScrollView
969
- [horizontal]="isHorizontal"
970
- [style]="resolvedStyle"
971
- [contentContainerStyle]="resolvedContentContainerStyle"
972
- [testID]="foldedAccessibility().testID"
973
- [nativeID]="foldedAccessibility().nativeID"
974
- [accessible]="foldedAccessibility().accessible"
975
- [accessibilityLabel]="foldedAccessibility().accessibilityLabel"
976
- [accessibilityHint]="foldedAccessibility().accessibilityHint"
977
- [accessibilityRole]="foldedAccessibility().accessibilityRole"
978
- [accessibilityState]="foldedAccessibility().accessibilityState"
979
- [accessibilityValue]="foldedAccessibility().accessibilityValue"
980
- [accessibilityActions]="foldedAccessibility().accessibilityActions"
981
- [accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
982
- [importantForAccessibility]="
983
- foldedAccessibility().importantForAccessibility
984
- "
985
- [accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
986
- [screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
987
- [accessibilityViewIsModal]="
988
- foldedAccessibility().accessibilityViewIsModal
989
- "
990
- [accessibilityElementsHidden]="
991
- foldedAccessibility().accessibilityElementsHidden
992
- "
993
- [accessibilityIgnoresInvertColors]="
994
- foldedAccessibility().accessibilityIgnoresInvertColors
995
- "
996
- [accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
997
- [accessibilityRespondsToUserInteraction]="
998
- foldedAccessibility().accessibilityRespondsToUserInteraction
999
- "
1000
- [accessibilityShowsLargeContentViewer]="
1001
- foldedAccessibility().accessibilityShowsLargeContentViewer
1002
- "
1003
- [accessibilityLargeContentTitle]="
1004
- foldedAccessibility().accessibilityLargeContentTitle
1005
- "
1006
- (accessibilityAction)="accessibilityActionTick($event)"
1007
- (accessibilityTap)="accessibilityTapTick($event)"
1008
- (magicTap)="magicTapTick($event)"
1009
- (accessibilityEscape)="accessibilityEscapeTick($event)"
1010
- [onScroll]="onScrollTick"
1011
- (layout)="onLayoutTick($event)"
1012
- [onScrollBeginDrag]="onScrollBeginDrag"
1013
- [onScrollEndDrag]="onScrollEndDrag"
1014
- [onMomentumScrollBegin]="onMomentumScrollBegin"
1015
- [onMomentumScrollEnd]="onMomentumScrollEnd"
1016
- [scrollEventThrottle]="scrollEventThrottle"
1017
- [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
1018
- [keyboardDismissMode]="keyboardDismissMode"
1019
- [contentOffset]="commandedOffset"
1020
- [stickyHeaderIndices]="renderedStickyIndices"
1021
- [maintainVisibleContentPosition]="resolvedMaintainVisibleContentPosition"
1022
- >
1023
- @if (shouldRenderRefreshControl) {
1024
- <RefreshControl
1025
- [refreshing]="refreshing ?? false"
1026
- (refresh)="handleRefresh()"
1027
- [progressViewOffset]="progressViewOffset"
1028
- />
1029
- }
1042
+ @if (isHorizontal) {
1043
+ <horizontal-scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
1044
+ @if (shouldRenderRefreshControl) {
1045
+ <refresh-control
1046
+ [symbioteHostProps]="refreshControlBag()"
1047
+ ></refresh-control>
1048
+ }
1030
1049
 
1031
- @if (headerDir !== undefined) {
1032
- <symbiote-view>
1033
- <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
1034
- </symbiote-view>
1035
- }
1050
+ @if (headerDir !== undefined) {
1051
+ <view>
1052
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
1053
+ </view>
1054
+ }
1036
1055
 
1037
- @if (itemCount === 0) {
1038
- @if (emptyDir !== undefined) {
1039
- <symbiote-view>
1040
- <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
1041
- </symbiote-view>
1056
+ @if (itemCount === 0) {
1057
+ @if (emptyDir !== undefined) {
1058
+ <view>
1059
+ <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
1060
+ </view>
1061
+ }
1062
+ } @else {
1063
+ @if (leadingSpacerStyle !== null) {
1064
+ <view [style]="leadingSpacerStyle"></view>
1065
+ }
1066
+ @if (forcedStickyCell !== null) {
1067
+ <view
1068
+ (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
1069
+ [style]="cellStyle"
1070
+ >
1071
+ <ng-container
1072
+ [vListOutlet]="itemDir?.templateRef"
1073
+ [vListOutletContext]="forcedStickyCell.context"
1074
+ ></ng-container>
1075
+ </view>
1076
+ }
1077
+ @if (gapSpacerStyle !== null) {
1078
+ <view [style]="gapSpacerStyle"></view>
1079
+ }
1080
+ @for (cell of windowCells; track cell.key) {
1081
+ <view
1082
+ (layout)="handleCellLayout(cell.measure, $event)"
1083
+ [style]="cellStyle"
1084
+ >
1085
+ <ng-container
1086
+ [vListOutlet]="itemDir?.templateRef"
1087
+ [vListOutletContext]="cell.context"
1088
+ ></ng-container>
1089
+ @if (cell.separatorContext !== undefined) {
1090
+ <ng-container
1091
+ [vListOutlet]="separatorDir?.templateRef"
1092
+ [vListOutletContext]="cell.separatorContext"
1093
+ ></ng-container>
1094
+ }
1095
+ </view>
1096
+ }
1097
+ @if (trailingSpacerStyle !== null) {
1098
+ <view [style]="trailingSpacerStyle"></view>
1099
+ }
1042
1100
  }
1043
- } @else {
1044
- @if (leadingSpacerStyle !== null) {
1045
- <symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
1101
+
1102
+ @if (footerDir !== undefined) {
1103
+ <view>
1104
+ <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
1105
+ </view>
1046
1106
  }
1047
- @if (forcedStickyCell !== null) {
1048
- <symbiote-view
1049
- (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
1050
- [style]="cellStyle"
1051
- >
1052
- <ng-container
1053
- [vListOutlet]="itemDir?.templateRef"
1054
- [vListOutletContext]="forcedStickyCell.context"
1055
- ></ng-container>
1056
- </symbiote-view>
1107
+ </horizontal-scroll-view>
1108
+ } @else {
1109
+ <scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
1110
+ @if (shouldRenderRefreshControl) {
1111
+ <refresh-control
1112
+ [symbioteHostProps]="refreshControlBag()"
1113
+ ></refresh-control>
1057
1114
  }
1058
- @if (gapSpacerStyle !== null) {
1059
- <symbiote-view [style]="gapSpacerStyle"></symbiote-view>
1115
+
1116
+ @if (headerDir !== undefined) {
1117
+ <view>
1118
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
1119
+ </view>
1060
1120
  }
1061
- <!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
1062
- (VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
1063
- the chrome between two cells is gap + separator + gap while a spacer collapsing that
1064
- region replaces it with a single gap — every cell below the leading spacer then lands
1065
- short by (separator + gap) and the content jumps by that much whenever the window's
1066
- first index moves. Measured at exactly 17px on device 2026-08-19; see
1067
- .claude/rules/list-geometry-feedback-loop.md. -->
1068
- @for (cell of windowCells; track cell.key) {
1069
- <symbiote-view
1070
- (layout)="handleCellLayout(cell.measure, $event)"
1071
- [style]="cellStyle"
1072
- >
1073
- <ng-container
1074
- [vListOutlet]="itemDir?.templateRef"
1075
- [vListOutletContext]="cell.context"
1076
- ></ng-container>
1077
- @if (cell.separatorContext !== undefined) {
1121
+
1122
+ @if (itemCount === 0) {
1123
+ @if (emptyDir !== undefined) {
1124
+ <view>
1125
+ <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
1126
+ </view>
1127
+ }
1128
+ } @else {
1129
+ @if (leadingSpacerStyle !== null) {
1130
+ <view [style]="leadingSpacerStyle"></view>
1131
+ }
1132
+ @if (forcedStickyCell !== null) {
1133
+ <view
1134
+ (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
1135
+ [style]="cellStyle"
1136
+ >
1078
1137
  <ng-container
1079
- [vListOutlet]="separatorDir?.templateRef"
1080
- [vListOutletContext]="cell.separatorContext"
1138
+ [vListOutlet]="itemDir?.templateRef"
1139
+ [vListOutletContext]="forcedStickyCell.context"
1081
1140
  ></ng-container>
1082
- }
1083
- </symbiote-view>
1084
- }
1085
- @if (trailingSpacerStyle !== null) {
1086
- <symbiote-view [style]="trailingSpacerStyle"></symbiote-view>
1141
+ </view>
1142
+ }
1143
+ @if (gapSpacerStyle !== null) {
1144
+ <view [style]="gapSpacerStyle"></view>
1145
+ }
1146
+ <!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
1147
+ (VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
1148
+ the chrome between two cells is gap + separator + gap while a spacer collapsing that
1149
+ region replaces it with a single gap — every cell below the leading spacer then lands
1150
+ short by (separator + gap) and the content jumps by that much whenever the window's
1151
+ first index moves. Measured at exactly 17px on device 2026-08-19; see
1152
+ .claude/rules/list-geometry-feedback-loop.md. -->
1153
+ @for (cell of windowCells; track cell.key) {
1154
+ <view
1155
+ (layout)="handleCellLayout(cell.measure, $event)"
1156
+ [style]="cellStyle"
1157
+ >
1158
+ <ng-container
1159
+ [vListOutlet]="itemDir?.templateRef"
1160
+ [vListOutletContext]="cell.context"
1161
+ ></ng-container>
1162
+ @if (cell.separatorContext !== undefined) {
1163
+ <ng-container
1164
+ [vListOutlet]="separatorDir?.templateRef"
1165
+ [vListOutletContext]="cell.separatorContext"
1166
+ ></ng-container>
1167
+ }
1168
+ </view>
1169
+ }
1170
+ @if (trailingSpacerStyle !== null) {
1171
+ <view [style]="trailingSpacerStyle"></view>
1172
+ }
1087
1173
  }
1088
- }
1089
1174
 
1090
- @if (footerDir !== undefined) {
1091
- <symbiote-view>
1092
- <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
1093
- </symbiote-view>
1094
- }
1095
- </ScrollView>
1175
+ @if (footerDir !== undefined) {
1176
+ <view>
1177
+ <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
1178
+ </view>
1179
+ }
1180
+ </scroll-view>
1181
+ }
1096
1182
  `,
1097
1183
  }]
1098
1184
  }], propDecorators: { endReached: [{
@@ -1265,8 +1351,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
1265
1351
  }], separatorDir: [{
1266
1352
  type: ContentChild,
1267
1353
  args: [VListSeparatorDirective]
1268
- }], scrollView: [{
1354
+ }], scrollHostRef: [{
1269
1355
  type: ViewChild,
1270
- args: [ScrollView]
1356
+ args: ['scrollHost', { read: ElementRef }]
1271
1357
  }] } });
1272
1358
  //# sourceMappingURL=index.js.map