@symbiote-native/angular 1.0.0 → 2.0.1

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 +54 -20
  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,7 +22,7 @@
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
 
27
27
  import {
28
28
  CUSTOM_ELEMENTS_SCHEMA,
@@ -80,20 +80,21 @@ import {
80
80
  type IViewabilityConfigCallbackPair,
81
81
  type IViewableItemsChangedInfo,
82
82
  type IVirtualizedListHandle,
83
+ buildScrollViewHandle,
83
84
  } from '@symbiote-native/components';
84
85
  import {
85
86
  dlog,
86
87
  flattenStyle,
88
+ isSymbioteNode,
87
89
  type IStyleProp,
88
90
  type ISymbioteEvent,
89
91
  type ISymbioteNode,
90
92
  type IViewStyle,
91
93
  } from '@symbiote-native/engine';
92
94
  import { countAngular } from '../../diagnostics';
93
- import { ScrollView } from '../scroll-view';
94
- import { RefreshControl } from '../refresh-control';
95
95
  import {
96
96
  anchorHostStyle,
97
+ SymbioteHostPropsDirective,
97
98
  SymbioteStyleInputDirective,
98
99
  ViewHost,
99
100
  } from '../../primitives';
@@ -130,8 +131,6 @@ export {
130
131
  import {
131
132
  gateWanted,
132
133
  injectGateDemandAbove,
133
- provideGateDemand,
134
- type IGateDemand,
135
134
  type IGatedAccessibilityEvent,
136
135
  } from '../../gate-demand';
137
136
  export type { IVListItemContext, IVListSeparatorContext } from './directives';
@@ -217,145 +216,162 @@ interface IWindowCell<ItemT> {
217
216
  separatorContext?: IVListSeparatorContext<ItemT>;
218
217
  }
219
218
 
219
+ // The scroll body is identical on both axes; only the outer tag differs (the axis comes from the
220
+ // TAG, never a prop — `<horizontal-scroll-view>` vs `<scroll-view>`, matching every other adapter,
221
+ // `.claude/rules/host-primitive-tier.md`). No shared-outlet primitive exists to de-duplicate this
222
+ // (this package's own NgTemplateOutlet twin is keyed on a per-cell context, not a static block, and
223
+ // ngc's partial evaluator needs the decorator's `template` to be one literal), so the body is
224
+ // duplicated across the two branches — the same shape Svelte's own `{#if horizontal}` takes.
220
225
  @Component({
221
226
  selector: 'VirtualizedList',
222
227
  standalone: true,
223
- viewProviders: [provideGateDemand(() => VirtualizedList)],
224
228
  hostDirectives: [
225
229
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
226
230
  ],
227
231
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
228
- imports: [ScrollView, RefreshControl, VListOutletDirective, ViewHost],
232
+ imports: [SymbioteHostPropsDirective, VListOutletDirective, ViewHost],
229
233
  changeDetection: ChangeDetectionStrategy.OnPush,
230
234
  template: `
231
- <ScrollView
232
- [horizontal]="isHorizontal"
233
- [style]="resolvedStyle"
234
- [contentContainerStyle]="resolvedContentContainerStyle"
235
- [testID]="foldedAccessibility().testID"
236
- [nativeID]="foldedAccessibility().nativeID"
237
- [accessible]="foldedAccessibility().accessible"
238
- [accessibilityLabel]="foldedAccessibility().accessibilityLabel"
239
- [accessibilityHint]="foldedAccessibility().accessibilityHint"
240
- [accessibilityRole]="foldedAccessibility().accessibilityRole"
241
- [accessibilityState]="foldedAccessibility().accessibilityState"
242
- [accessibilityValue]="foldedAccessibility().accessibilityValue"
243
- [accessibilityActions]="foldedAccessibility().accessibilityActions"
244
- [accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
245
- [importantForAccessibility]="
246
- foldedAccessibility().importantForAccessibility
247
- "
248
- [accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
249
- [screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
250
- [accessibilityViewIsModal]="
251
- foldedAccessibility().accessibilityViewIsModal
252
- "
253
- [accessibilityElementsHidden]="
254
- foldedAccessibility().accessibilityElementsHidden
255
- "
256
- [accessibilityIgnoresInvertColors]="
257
- foldedAccessibility().accessibilityIgnoresInvertColors
258
- "
259
- [accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
260
- [accessibilityRespondsToUserInteraction]="
261
- foldedAccessibility().accessibilityRespondsToUserInteraction
262
- "
263
- [accessibilityShowsLargeContentViewer]="
264
- foldedAccessibility().accessibilityShowsLargeContentViewer
265
- "
266
- [accessibilityLargeContentTitle]="
267
- foldedAccessibility().accessibilityLargeContentTitle
268
- "
269
- (accessibilityAction)="accessibilityActionTick($event)"
270
- (accessibilityTap)="accessibilityTapTick($event)"
271
- (magicTap)="magicTapTick($event)"
272
- (accessibilityEscape)="accessibilityEscapeTick($event)"
273
- [onScroll]="onScrollTick"
274
- (layout)="onLayoutTick($event)"
275
- [onScrollBeginDrag]="onScrollBeginDrag"
276
- [onScrollEndDrag]="onScrollEndDrag"
277
- [onMomentumScrollBegin]="onMomentumScrollBegin"
278
- [onMomentumScrollEnd]="onMomentumScrollEnd"
279
- [scrollEventThrottle]="scrollEventThrottle"
280
- [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
281
- [keyboardDismissMode]="keyboardDismissMode"
282
- [contentOffset]="commandedOffset"
283
- [stickyHeaderIndices]="renderedStickyIndices"
284
- [maintainVisibleContentPosition]="resolvedMaintainVisibleContentPosition"
285
- >
286
- @if (shouldRenderRefreshControl) {
287
- <RefreshControl
288
- [refreshing]="refreshing ?? false"
289
- (refresh)="handleRefresh()"
290
- [progressViewOffset]="progressViewOffset"
291
- />
292
- }
235
+ @if (isHorizontal) {
236
+ <horizontal-scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
237
+ @if (shouldRenderRefreshControl) {
238
+ <refresh-control
239
+ [symbioteHostProps]="refreshControlBag()"
240
+ ></refresh-control>
241
+ }
293
242
 
294
- @if (headerDir !== undefined) {
295
- <symbiote-view>
296
- <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
297
- </symbiote-view>
298
- }
243
+ @if (headerDir !== undefined) {
244
+ <view>
245
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
246
+ </view>
247
+ }
299
248
 
300
- @if (itemCount === 0) {
301
- @if (emptyDir !== undefined) {
302
- <symbiote-view>
303
- <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
304
- </symbiote-view>
249
+ @if (itemCount === 0) {
250
+ @if (emptyDir !== undefined) {
251
+ <view>
252
+ <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
253
+ </view>
254
+ }
255
+ } @else {
256
+ @if (leadingSpacerStyle !== null) {
257
+ <view [style]="leadingSpacerStyle"></view>
258
+ }
259
+ @if (forcedStickyCell !== null) {
260
+ <view
261
+ (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
262
+ [style]="cellStyle"
263
+ >
264
+ <ng-container
265
+ [vListOutlet]="itemDir?.templateRef"
266
+ [vListOutletContext]="forcedStickyCell.context"
267
+ ></ng-container>
268
+ </view>
269
+ }
270
+ @if (gapSpacerStyle !== null) {
271
+ <view [style]="gapSpacerStyle"></view>
272
+ }
273
+ @for (cell of windowCells; track cell.key) {
274
+ <view
275
+ (layout)="handleCellLayout(cell.measure, $event)"
276
+ [style]="cellStyle"
277
+ >
278
+ <ng-container
279
+ [vListOutlet]="itemDir?.templateRef"
280
+ [vListOutletContext]="cell.context"
281
+ ></ng-container>
282
+ @if (cell.separatorContext !== undefined) {
283
+ <ng-container
284
+ [vListOutlet]="separatorDir?.templateRef"
285
+ [vListOutletContext]="cell.separatorContext"
286
+ ></ng-container>
287
+ }
288
+ </view>
289
+ }
290
+ @if (trailingSpacerStyle !== null) {
291
+ <view [style]="trailingSpacerStyle"></view>
292
+ }
305
293
  }
306
- } @else {
307
- @if (leadingSpacerStyle !== null) {
308
- <symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
294
+
295
+ @if (footerDir !== undefined) {
296
+ <view>
297
+ <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
298
+ </view>
309
299
  }
310
- @if (forcedStickyCell !== null) {
311
- <symbiote-view
312
- (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
313
- [style]="cellStyle"
314
- >
315
- <ng-container
316
- [vListOutlet]="itemDir?.templateRef"
317
- [vListOutletContext]="forcedStickyCell.context"
318
- ></ng-container>
319
- </symbiote-view>
300
+ </horizontal-scroll-view>
301
+ } @else {
302
+ <scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
303
+ @if (shouldRenderRefreshControl) {
304
+ <refresh-control
305
+ [symbioteHostProps]="refreshControlBag()"
306
+ ></refresh-control>
320
307
  }
321
- @if (gapSpacerStyle !== null) {
322
- <symbiote-view [style]="gapSpacerStyle"></symbiote-view>
308
+
309
+ @if (headerDir !== undefined) {
310
+ <view>
311
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
312
+ </view>
323
313
  }
324
- <!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
325
- (VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
326
- the chrome between two cells is gap + separator + gap while a spacer collapsing that
327
- region replaces it with a single gap — every cell below the leading spacer then lands
328
- short by (separator + gap) and the content jumps by that much whenever the window's
329
- first index moves. Measured at exactly 17px on device 2026-08-19; see
330
- .claude/rules/list-geometry-feedback-loop.md. -->
331
- @for (cell of windowCells; track cell.key) {
332
- <symbiote-view
333
- (layout)="handleCellLayout(cell.measure, $event)"
334
- [style]="cellStyle"
335
- >
336
- <ng-container
337
- [vListOutlet]="itemDir?.templateRef"
338
- [vListOutletContext]="cell.context"
339
- ></ng-container>
340
- @if (cell.separatorContext !== undefined) {
314
+
315
+ @if (itemCount === 0) {
316
+ @if (emptyDir !== undefined) {
317
+ <view>
318
+ <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
319
+ </view>
320
+ }
321
+ } @else {
322
+ @if (leadingSpacerStyle !== null) {
323
+ <view [style]="leadingSpacerStyle"></view>
324
+ }
325
+ @if (forcedStickyCell !== null) {
326
+ <view
327
+ (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
328
+ [style]="cellStyle"
329
+ >
341
330
  <ng-container
342
- [vListOutlet]="separatorDir?.templateRef"
343
- [vListOutletContext]="cell.separatorContext"
331
+ [vListOutlet]="itemDir?.templateRef"
332
+ [vListOutletContext]="forcedStickyCell.context"
344
333
  ></ng-container>
345
- }
346
- </symbiote-view>
347
- }
348
- @if (trailingSpacerStyle !== null) {
349
- <symbiote-view [style]="trailingSpacerStyle"></symbiote-view>
334
+ </view>
335
+ }
336
+ @if (gapSpacerStyle !== null) {
337
+ <view [style]="gapSpacerStyle"></view>
338
+ }
339
+ <!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
340
+ (VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
341
+ the chrome between two cells is gap + separator + gap while a spacer collapsing that
342
+ region replaces it with a single gap — every cell below the leading spacer then lands
343
+ short by (separator + gap) and the content jumps by that much whenever the window's
344
+ first index moves. Measured at exactly 17px on device 2026-08-19; see
345
+ .claude/rules/list-geometry-feedback-loop.md. -->
346
+ @for (cell of windowCells; track cell.key) {
347
+ <view
348
+ (layout)="handleCellLayout(cell.measure, $event)"
349
+ [style]="cellStyle"
350
+ >
351
+ <ng-container
352
+ [vListOutlet]="itemDir?.templateRef"
353
+ [vListOutletContext]="cell.context"
354
+ ></ng-container>
355
+ @if (cell.separatorContext !== undefined) {
356
+ <ng-container
357
+ [vListOutlet]="separatorDir?.templateRef"
358
+ [vListOutletContext]="cell.separatorContext"
359
+ ></ng-container>
360
+ }
361
+ </view>
362
+ }
363
+ @if (trailingSpacerStyle !== null) {
364
+ <view [style]="trailingSpacerStyle"></view>
365
+ }
350
366
  }
351
- }
352
367
 
353
- @if (footerDir !== undefined) {
354
- <symbiote-view>
355
- <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
356
- </symbiote-view>
357
- }
358
- </ScrollView>
368
+ @if (footerDir !== undefined) {
369
+ <view>
370
+ <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
371
+ </view>
372
+ }
373
+ </scroll-view>
374
+ }
359
375
  `,
360
376
  })
361
377
  export class VirtualizedList<ItemT = unknown>
@@ -369,7 +385,7 @@ export class VirtualizedList<ItemT = unknown>
369
385
  {
370
386
  // The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
371
387
  // `[onEndReached]="…"`. See handleRefresh/accessibility*Tick below for how the still-callback-
372
- // shaped ScrollView/RefreshControl @Input()s are fed from these.
388
+ // shaped scroll-view/refresh-control bag keys are fed from these.
373
389
  @Output() readonly endReached = new EventEmitter<{
374
390
  distanceFromEnd: number;
375
391
  }>();
@@ -493,10 +509,24 @@ export class VirtualizedList<ItemT = unknown>
493
509
  @ContentChild(VListSeparatorDirective)
494
510
  separatorDir?: VListSeparatorDirective<ItemT>;
495
511
 
496
- // The composed inner scroll view. Its instance IS an IScrollViewHandle (scrollTo / scrollToEnd /
497
- // flashScrollIndicators / getScrollNode), so the imperative handle delegates straight to it.
498
- // Available from ngAfterViewInit on; the handle reads the node lazily and no-ops before commit.
499
- @ViewChild(ScrollView) private scrollView?: ScrollView;
512
+ // The inner scroll TAG's own engine node — a template ref on a bare intrinsic hands back the
513
+ // host node directly (`isSymbioteNode(elementRef.nativeElement)`, matching `anchorHostStyle`'s
514
+ // own reader below), so there is no wrapper component left to read a handle off. Present from
515
+ // whichever branch (`scroll-view` / `horizontal-scroll-view`) is currently mounted, once the
516
+ // template has run at least one CD pass.
517
+ @ViewChild('scrollHost', { read: ElementRef })
518
+ private scrollHostRef?: ElementRef<unknown>;
519
+
520
+ private get scrollNode(): ISymbioteNode | null {
521
+ const node = this.scrollHostRef?.nativeElement;
522
+ return isSymbioteNode(node) ? node : null;
523
+ }
524
+
525
+ // `buildScrollViewHandle` reads the node lazily through the getter above and no-ops before
526
+ // commit — the same function every other adapter feeds from its own host-instance accessor.
527
+ private readonly scrollHandle: IScrollViewHandle = buildScrollViewHandle(
528
+ () => this.scrollNode,
529
+ );
500
530
 
501
531
  // --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
502
532
  itemCount = EMPTY_OFFSET;
@@ -578,13 +608,13 @@ export class VirtualizedList<ItemT = unknown>
578
608
 
579
609
  private readonly cdr = inject(ChangeDetectorRef);
580
610
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
581
- // onto (see anchorHostStyle's doc comment) — NOT `scrollView` above, which targets the real
582
- // inner `<ScrollView>` one level down (itself its own separate anchor host).
611
+ // onto (see anchorHostStyle's doc comment) — NOT `scrollHostRef` above, which targets the real
612
+ // inner `<scroll-view>`/`<horizontal-scroll-view>` tag one level down.
583
613
  private readonly elementRef = inject(ElementRef);
584
614
 
585
- // Bridges the non-reactive @Input fields into the reactive graph so the computed below can
586
- // memoize a bag derived from them (the same bridge ScrollView's shared.ts uses - read its comment
587
- // for why signal inputs are not an option while this package's unit suite runs on JIT).
615
+ // Bridges the non-reactive @Input fields into the reactive graph so `foldedAccessibility` below
616
+ // can memoize a bag derived from them (read its own comment for why signal inputs are not an
617
+ // option while this package's unit suite runs on JIT).
588
618
  //
589
619
  // ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
590
620
  // (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
@@ -593,12 +623,13 @@ export class VirtualizedList<ItemT = unknown>
593
623
  // instead of widening this one.
594
624
  private readonly inputsRevision = signal(0);
595
625
 
596
- // MEASURED: the template feeds 20 separate <ScrollView> inputs off this one bag, and Angular does
597
- // not cache a getter across binding expressions - as a getter it rebuilt the object 20 times per
598
- // refresh, and every consumer saw a fresh reference, so all 20 reported "changed" and wrote
599
- // through to the ScrollView. A computed evaluates once and hands back the SAME object until an
600
- // input actually changes. Every dependency below is an @Input, which is what makes
601
- // inputsRevision a complete dependency set here.
626
+ // MEASURED (while ScrollView was still a wrapper): the template fed 20 separate inputs off this
627
+ // one bag, and Angular does not cache a getter across binding expressions - as a getter it
628
+ // rebuilt the object 20 times per refresh, and every consumer saw a fresh reference, so all 20
629
+ // reported "changed" and wrote through. A computed evaluates once and hands back the SAME object
630
+ // until an input actually changes. Every dependency below is an @Input, which is what makes
631
+ // inputsRevision a complete dependency set here — `scrollViewBag()` reuses this result rather
632
+ // than re-running the fold.
602
633
  readonly foldedAccessibility = computed<
603
634
  IAccessibilityProps & IAriaProps & Record<string, unknown>
604
635
  >(() => {
@@ -648,6 +679,57 @@ export class VirtualizedList<ItemT = unknown>
648
679
  return this.refreshRequested ?? this.refresh.observed;
649
680
  }
650
681
 
682
+ // The flat bag fed to the scroll tag via `[symbioteHostProps]`. A plain method, not `computed()`:
683
+ // most of these fields (resolvedStyle, renderedStickyIndices, commandedOffset, …) are recomputed
684
+ // in ngDoCheck/dispatch as ordinary fields, not signals, so a computed() memoized only against
685
+ // `inputsRevision` would go stale the moment one of them changed outside an @Input() write. The
686
+ // directive itself diffs per key before writing to the engine (`primitives/shared.ts`), so a
687
+ // fresh object every CD pass costs nothing beyond the literal allocation.
688
+ scrollViewBag(): Record<string, unknown> {
689
+ return {
690
+ ...this.foldedAccessibility(),
691
+ style: this.resolvedStyle,
692
+ contentContainerStyle: this.resolvedContentContainerStyle,
693
+ onScroll: this.onScrollTick,
694
+ onLayout: this.onLayoutTick,
695
+ onScrollBeginDrag: this.onScrollBeginDrag,
696
+ onScrollEndDrag: this.onScrollEndDrag,
697
+ onMomentumScrollBegin: this.onMomentumScrollBegin,
698
+ onMomentumScrollEnd: this.onMomentumScrollEnd,
699
+ scrollEventThrottle: this.scrollEventThrottle,
700
+ keyboardShouldPersistTaps: this.keyboardShouldPersistTaps,
701
+ keyboardDismissMode: this.keyboardDismissMode,
702
+ contentOffset: this.commandedOffset,
703
+ stickyHeaderIndices: this.renderedStickyIndices,
704
+ maintainVisibleContentPosition:
705
+ this.resolvedMaintainVisibleContentPosition,
706
+ // The four boolean-gated Fabric events (`.claude/rules/fabric-boolean-event-gates.md`): the
707
+ // TAG has no `.observed` of its own, so `wantsGate` (own subscriber, or the demand from
708
+ // whatever wraps this list) is what decides whether the flag reaches Fabric at all.
709
+ onAccessibilityAction: this.wantsGate('accessibilityAction')
710
+ ? this.accessibilityActionTick
711
+ : undefined,
712
+ onAccessibilityTap: this.wantsGate('accessibilityTap')
713
+ ? this.accessibilityTapTick
714
+ : undefined,
715
+ onMagicTap: this.wantsGate('magicTap') ? this.magicTapTick : undefined,
716
+ onAccessibilityEscape: this.wantsGate('accessibilityEscape')
717
+ ? this.accessibilityEscapeTick
718
+ : undefined,
719
+ };
720
+ }
721
+
722
+ // The `refresh-control` tag's own bag — an ordinary first child, re-parented per platform by the
723
+ // scroll-view behavior (`../refresh-control-props.ts`). `onRefresh` is a plain listener prop, not
724
+ // a gated one: RefreshControl's `refresh` is unaffected by `fabric-boolean-event-gates.md`.
725
+ refreshControlBag(): Record<string, unknown> {
726
+ return {
727
+ refreshing: this.refreshing ?? false,
728
+ progressViewOffset: this.progressViewOffset,
729
+ onRefresh: this.handleRefresh,
730
+ };
731
+ }
732
+
651
733
  get isHorizontal(): boolean {
652
734
  return this.horizontal === true;
653
735
  }
@@ -675,9 +757,10 @@ export class VirtualizedList<ItemT = unknown>
675
757
  return this.onStartReachedThreshold ?? DEFAULT_START_REACHED_THRESHOLD;
676
758
  }
677
759
 
678
- // ScrollView's onScroll stays an @Input() callback (an Animated.event(...) target must be able to
679
- // flow through it, which an @Output() can't carry), so this is a stable arrow field passed
680
- // straight to [onScroll]. onLayout is a real @Output() now, bound via (layout)="onLayoutTick($event)".
760
+ // onScroll stays a plain callback bag key (an Animated.event(...) target must be able to flow
761
+ // through it, which an @Output() can't carry), so this is a stable arrow field read straight into
762
+ // scrollViewBag()'s `onScroll`. onLayout is unconditional too — this list always needs its own
763
+ // layout for windowing, regardless of whether an app subscribes to anything.
681
764
  onScrollTick = (event: ISymbioteEvent): void => {
682
765
  countAngular('scrollTicks');
683
766
  const offset = readScrollOffset(event, this.isHorizontal);
@@ -697,9 +780,9 @@ export class VirtualizedList<ItemT = unknown>
697
780
  this.dispatch({ kind: 'layout', length });
698
781
  };
699
782
 
700
- // RefreshControl's refresh and ScrollView's accessibility events are real @Output()s now, bound
701
- // via (refresh)="handleRefresh()" / (accessibilityAction)="accessibilityActionTick($event)" etc.
702
- // — these fields just adapt VirtualizedList's own @Output() into a plain re-emit callback.
783
+ // RefreshControl's refresh and the scroll tag's accessibility events reach here as plain
784
+ // listener-prop values in scrollViewBag()/refreshControlBag() — these fields just adapt
785
+ // VirtualizedList's own @Output() into a plain re-emit callback.
703
786
  handleRefresh = (): void => {
704
787
  this.refresh.emit();
705
788
  };
@@ -1263,23 +1346,23 @@ export class VirtualizedList<ItemT = unknown>
1263
1346
  }
1264
1347
 
1265
1348
  flashScrollIndicators(): void {
1266
- this.scrollView?.flashScrollIndicators();
1349
+ this.scrollHandle.flashScrollIndicators();
1267
1350
  }
1268
1351
 
1269
1352
  getNativeScrollRef(): IScrollViewHandle | null {
1270
- return this.scrollView ?? null;
1353
+ return this.scrollNode !== null ? this.scrollHandle : null;
1271
1354
  }
1272
1355
 
1273
1356
  getScrollableNode(): IScrollViewHandle | null {
1274
- return this.scrollView ?? null;
1357
+ return this.scrollNode !== null ? this.scrollHandle : null;
1275
1358
  }
1276
1359
 
1277
1360
  getScrollResponder(): IScrollViewHandle | null {
1278
- return this.scrollView ?? null;
1361
+ return this.scrollNode !== null ? this.scrollHandle : null;
1279
1362
  }
1280
1363
 
1281
1364
  getScrollNode(): ISymbioteNode | null {
1282
- return this.scrollView?.getScrollNode() ?? null;
1365
+ return this.scrollHandle.getScrollNode();
1283
1366
  }
1284
1367
 
1285
1368
  recordInteraction(): void {
@@ -1291,11 +1374,11 @@ export class VirtualizedList<ItemT = unknown>
1291
1374
  const target = this.isHorizontal
1292
1375
  ? { x: clamped, y: EMPTY_OFFSET }
1293
1376
  : { x: EMPTY_OFFSET, y: clamped };
1294
- if (this.scrollView !== undefined) {
1377
+ if (this.scrollNode !== null) {
1295
1378
  dlog(
1296
1379
  `Angular VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${this.isHorizontal})`,
1297
1380
  );
1298
- this.scrollView.scrollTo({ x: target.x, y: target.y, animated });
1381
+ this.scrollHandle.scrollTo({ x: target.x, y: target.y, animated });
1299
1382
  return;
1300
1383
  }
1301
1384
  dlog(`Angular VirtualizedList scrollTo offset=${clamped} pending-ref`);