@symbiote-native/angular 0.8.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 (256) hide show
  1. package/README.md +13 -10
  2. package/babel-register-composed.cjs +58 -18
  3. package/bin/symbiote-angular-dev.cjs +16 -4
  4. package/build/angular/anchor-host-registry.js +1 -15
  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} +4 -4
  22. package/build/angular/components/{image/shared.js → image-shared.js} +39 -28
  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.d.ts +2 -1
  28. package/build/angular/components/keyboard-avoiding-view/index.js +28 -27
  29. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  30. package/build/angular/components/modal/index.d.ts +2 -0
  31. package/build/angular/components/modal/index.js +33 -20
  32. package/build/angular/components/modal/index.js.map +1 -1
  33. package/build/angular/components/pressable-props.d.ts +32 -0
  34. package/build/angular/components/pressable-props.js +11 -0
  35. package/build/angular/components/pressable-props.js.map +1 -0
  36. package/build/angular/components/refresh-control-props.d.ts +16 -0
  37. package/build/angular/components/refresh-control-props.js +14 -0
  38. package/build/angular/components/refresh-control-props.js.map +1 -0
  39. package/build/angular/components/safe-area-view-props.d.ts +7 -0
  40. package/build/angular/components/safe-area-view-props.js +11 -0
  41. package/build/angular/components/safe-area-view-props.js.map +1 -0
  42. package/build/angular/components/scroll-view-props.d.ts +76 -0
  43. package/build/angular/components/scroll-view-props.js +18 -0
  44. package/build/angular/components/scroll-view-props.js.map +1 -0
  45. package/build/angular/components/section-list/index.d.ts +3 -0
  46. package/build/angular/components/section-list/index.js +10 -1
  47. package/build/angular/components/section-list/index.js.map +1 -1
  48. package/build/angular/components/text-input-props.d.ts +45 -0
  49. package/build/angular/components/text-input-props.js +18 -0
  50. package/build/angular/components/text-input-props.js.map +1 -0
  51. package/build/angular/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +6 -0
  52. package/build/angular/components/touchable-native-feedback/touchable-native-feedback-props.js +2 -0
  53. package/build/angular/components/touchable-native-feedback/touchable-native-feedback-props.js.map +1 -0
  54. package/build/angular/components/touchable-props.d.ts +14 -0
  55. package/build/angular/components/touchable-props.js +13 -0
  56. package/build/angular/components/touchable-props.js.map +1 -0
  57. package/build/angular/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +3 -0
  58. package/build/angular/components/touchable-without-feedback/touchable-without-feedback-props.js +2 -0
  59. package/build/angular/components/touchable-without-feedback/touchable-without-feedback-props.js.map +1 -0
  60. package/build/angular/components/virtualized-list/index.d.ts +8 -1
  61. package/build/angular/components/virtualized-list/index.js +373 -278
  62. package/build/angular/components/virtualized-list/index.js.map +1 -1
  63. package/build/angular/components/virtualized-section-list/index.d.ts +3 -0
  64. package/build/angular/components/virtualized-section-list/index.js +10 -1
  65. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  66. package/build/angular/components.d.ts +21 -27
  67. package/build/angular/components.js +13 -15
  68. package/build/angular/components.js.map +1 -1
  69. package/build/angular/create-portal/index.d.ts +11 -2
  70. package/build/angular/create-portal/index.js +37 -11
  71. package/build/angular/create-portal/index.js.map +1 -1
  72. package/build/angular/create-tunnel/index.js +1 -1
  73. package/build/angular/element-props.d.ts +53 -0
  74. package/build/angular/element-props.js +2 -0
  75. package/build/angular/element-props.js.map +1 -0
  76. package/build/angular/elements.d.ts +528 -0
  77. package/build/angular/elements.js +1444 -0
  78. package/build/angular/elements.js.map +1 -0
  79. package/build/angular/gate-demand.d.ts +37 -0
  80. package/build/angular/gate-demand.js +84 -0
  81. package/build/angular/gate-demand.js.map +1 -0
  82. package/build/angular/host-instance/index.js +1 -1
  83. package/build/angular/index.d.ts +5 -2
  84. package/build/angular/index.js +16 -1
  85. package/build/angular/index.js.map +1 -1
  86. package/build/angular/modules/animated/create-animated-component.d.ts +6 -6
  87. package/build/angular/modules/animated/create-animated-component.js +73 -81
  88. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  89. package/build/angular/modules/animated/index.js +1 -1
  90. package/build/angular/modules/animated/index.js.map +1 -1
  91. package/build/angular/modules/image/index.d.ts +3 -0
  92. package/build/angular/modules/image/index.js +16 -0
  93. package/build/angular/modules/image/index.js.map +1 -0
  94. package/build/angular/primitives/index.d.ts +28 -16
  95. package/build/angular/primitives/index.js +77 -31
  96. package/build/angular/primitives/index.js.map +1 -1
  97. package/build/angular/primitives/shared.d.ts +2 -3
  98. package/build/angular/primitives/shared.js +8 -53
  99. package/build/angular/primitives/shared.js.map +1 -1
  100. package/build/angular/register.d.ts +1 -0
  101. package/build/angular/register.js +16 -0
  102. package/build/angular/register.js.map +1 -0
  103. package/build/angular/render/index.js +4 -2
  104. package/build/angular/render/index.js.map +1 -1
  105. package/build/angular/renderer/index.js +207 -60
  106. package/build/angular/renderer/index.js.map +1 -1
  107. package/build/angular/renderer/value-change.d.ts +2 -0
  108. package/build/angular/renderer/value-change.js +9 -0
  109. package/build/angular/renderer/value-change.js.map +1 -0
  110. package/metro-config.cjs +23 -5
  111. package/metro-css-parser.cjs +2 -1
  112. package/package.json +17 -7
  113. package/src/anchor-host-registry.ts +1 -15
  114. package/src/change-detection-flush.ts +120 -0
  115. package/src/components/activity-indicator-props.ts +21 -0
  116. package/src/components/button-props.ts +26 -0
  117. package/src/components/flat-list/index.ts +12 -4
  118. package/src/components/image-background-props.ts +24 -0
  119. package/src/components/{image/shared.ts → image-shared.ts} +57 -26
  120. package/src/components/input-accessory-view-props.ts +21 -0
  121. package/src/components/keyboard-avoiding-view/index.ts +27 -16
  122. package/src/components/modal/index.ts +32 -11
  123. package/src/components/pressable-props.ts +73 -0
  124. package/src/components/refresh-control-props.ts +43 -0
  125. package/src/components/safe-area-view-props.ts +28 -0
  126. package/src/components/scroll-view-props.ts +109 -0
  127. package/src/components/section-list/index.ts +18 -0
  128. package/src/components/text-input-props.ts +72 -0
  129. package/src/components/touchable-native-feedback/touchable-native-feedback-props.ts +23 -0
  130. package/src/components/touchable-props.ts +30 -0
  131. package/src/components/touchable-without-feedback/touchable-without-feedback-props.ts +21 -0
  132. package/src/components/virtualized-list/index.ts +254 -153
  133. package/src/components/virtualized-section-list/index.ts +18 -0
  134. package/src/components.ts +86 -35
  135. package/src/create-portal/index.ts +39 -9
  136. package/src/create-tunnel/index.ts +1 -1
  137. package/src/diagnostics.ts +1 -1
  138. package/src/element-props.ts +90 -0
  139. package/src/elements.ts +919 -0
  140. package/src/gate-demand.ts +109 -0
  141. package/src/host-instance/index.ts +1 -1
  142. package/src/index.ts +54 -15
  143. package/src/modules/animated/create-animated-component.ts +73 -59
  144. package/src/modules/animated/index.ts +1 -1
  145. package/src/modules/image/index.ts +18 -0
  146. package/src/primitives/index.ts +47 -16
  147. package/src/primitives/shared.ts +11 -44
  148. package/src/register.ts +15 -0
  149. package/src/render/index.ts +4 -2
  150. package/src/renderer/index.ts +239 -80
  151. package/src/renderer/value-change.ts +8 -0
  152. package/build/angular/components/activity-indicator/index.android.d.ts +0 -13
  153. package/build/angular/components/activity-indicator/index.android.js +0 -29
  154. package/build/angular/components/activity-indicator/index.android.js.map +0 -1
  155. package/build/angular/components/activity-indicator/index.d.ts +0 -1
  156. package/build/angular/components/activity-indicator/index.ios.d.ts +0 -9
  157. package/build/angular/components/activity-indicator/index.ios.js +0 -26
  158. package/build/angular/components/activity-indicator/index.ios.js.map +0 -1
  159. package/build/angular/components/activity-indicator/index.js +0 -4
  160. package/build/angular/components/activity-indicator/index.js.map +0 -1
  161. package/build/angular/components/activity-indicator/shared.d.ts +0 -67
  162. package/build/angular/components/activity-indicator/shared.js +0 -260
  163. package/build/angular/components/activity-indicator/shared.js.map +0 -1
  164. package/build/angular/components/button.d.ts +0 -65
  165. package/build/angular/components/button.js +0 -351
  166. package/build/angular/components/button.js.map +0 -1
  167. package/build/angular/components/image/index.android.d.ts +0 -15
  168. package/build/angular/components/image/index.android.js +0 -91
  169. package/build/angular/components/image/index.android.js.map +0 -1
  170. package/build/angular/components/image/index.d.ts +0 -1
  171. package/build/angular/components/image/index.ios.d.ts +0 -15
  172. package/build/angular/components/image/index.ios.js +0 -91
  173. package/build/angular/components/image/index.ios.js.map +0 -1
  174. package/build/angular/components/image/index.js +0 -3
  175. package/build/angular/components/image/index.js.map +0 -1
  176. package/build/angular/components/image/shared.js.map +0 -1
  177. package/build/angular/components/image-background.d.ts +0 -82
  178. package/build/angular/components/image-background.js +0 -434
  179. package/build/angular/components/image-background.js.map +0 -1
  180. package/build/angular/components/input-accessory-view/index.d.ts +0 -65
  181. package/build/angular/components/input-accessory-view/index.js +0 -290
  182. package/build/angular/components/input-accessory-view/index.js.map +0 -1
  183. package/build/angular/components/pressable/index.d.ts +0 -129
  184. package/build/angular/components/pressable/index.js +0 -473
  185. package/build/angular/components/pressable/index.js.map +0 -1
  186. package/build/angular/components/refresh-control/index.d.ts +0 -90
  187. package/build/angular/components/refresh-control/index.js +0 -380
  188. package/build/angular/components/refresh-control/index.js.map +0 -1
  189. package/build/angular/components/safe-area-view/index.d.ts +0 -62
  190. package/build/angular/components/safe-area-view/index.js +0 -287
  191. package/build/angular/components/safe-area-view/index.js.map +0 -1
  192. package/build/angular/components/scroll-view/index.android.d.ts +0 -20
  193. package/build/angular/components/scroll-view/index.android.js +0 -262
  194. package/build/angular/components/scroll-view/index.android.js.map +0 -1
  195. package/build/angular/components/scroll-view/index.d.ts +0 -3
  196. package/build/angular/components/scroll-view/index.ios.d.ts +0 -8
  197. package/build/angular/components/scroll-view/index.ios.js +0 -92
  198. package/build/angular/components/scroll-view/index.ios.js.map +0 -1
  199. package/build/angular/components/scroll-view/index.js +0 -6
  200. package/build/angular/components/scroll-view/index.js.map +0 -1
  201. package/build/angular/components/scroll-view/projection.d.ts +0 -67
  202. package/build/angular/components/scroll-view/projection.js +0 -560
  203. package/build/angular/components/scroll-view/projection.js.map +0 -1
  204. package/build/angular/components/scroll-view/shared.d.ts +0 -239
  205. package/build/angular/components/scroll-view/shared.js +0 -832
  206. package/build/angular/components/scroll-view/shared.js.map +0 -1
  207. package/build/angular/components/scroll-view/sticky-header.d.ts +0 -40
  208. package/build/angular/components/scroll-view/sticky-header.js +0 -224
  209. package/build/angular/components/scroll-view/sticky-header.js.map +0 -1
  210. package/build/angular/components/switch/index.android.d.ts +0 -24
  211. package/build/angular/components/switch/index.android.js +0 -112
  212. package/build/angular/components/switch/index.android.js.map +0 -1
  213. package/build/angular/components/switch/index.d.ts +0 -1
  214. package/build/angular/components/switch/index.ios.d.ts +0 -23
  215. package/build/angular/components/switch/index.ios.js +0 -111
  216. package/build/angular/components/switch/index.ios.js.map +0 -1
  217. package/build/angular/components/switch/index.js +0 -4
  218. package/build/angular/components/switch/index.js.map +0 -1
  219. package/build/angular/components/switch/shared.d.ts +0 -49
  220. package/build/angular/components/switch/shared.js +0 -191
  221. package/build/angular/components/switch/shared.js.map +0 -1
  222. package/build/angular/components/text-input.d.ts +0 -161
  223. package/build/angular/components/text-input.js +0 -659
  224. package/build/angular/components/text-input.js.map +0 -1
  225. package/build/angular/components/touchable/index.d.ts +0 -276
  226. package/build/angular/components/touchable/index.js +0 -1422
  227. package/build/angular/components/touchable/index.js.map +0 -1
  228. package/build/angular/components/touchable-native-feedback/index.d.ts +0 -86
  229. package/build/angular/components/touchable-native-feedback/index.js +0 -407
  230. package/build/angular/components/touchable-native-feedback/index.js.map +0 -1
  231. package/src/components/activity-indicator/index.android.ts +0 -23
  232. package/src/components/activity-indicator/index.ios.ts +0 -21
  233. package/src/components/activity-indicator/index.ts +0 -4
  234. package/src/components/activity-indicator/shared.ts +0 -225
  235. package/src/components/button.ts +0 -220
  236. package/src/components/image/index.android.ts +0 -99
  237. package/src/components/image/index.ios.ts +0 -99
  238. package/src/components/image/index.ts +0 -3
  239. package/src/components/image-background.ts +0 -290
  240. package/src/components/input-accessory-view/index.ts +0 -240
  241. package/src/components/pressable/index.ts +0 -462
  242. package/src/components/refresh-control/index.ts +0 -340
  243. package/src/components/safe-area-view/index.ts +0 -231
  244. package/src/components/scroll-view/index.android.ts +0 -205
  245. package/src/components/scroll-view/index.ios.ts +0 -81
  246. package/src/components/scroll-view/index.ts +0 -7
  247. package/src/components/scroll-view/projection.ts +0 -713
  248. package/src/components/scroll-view/shared.ts +0 -1066
  249. package/src/components/scroll-view/sticky-header.ts +0 -267
  250. package/src/components/switch/index.android.ts +0 -113
  251. package/src/components/switch/index.ios.ts +0 -112
  252. package/src/components/switch/index.ts +0 -4
  253. package/src/components/switch/shared.ts +0 -284
  254. package/src/components/text-input.ts +0 -613
  255. package/src/components/touchable/index.ts +0 -913
  256. package/src/components/touchable-native-feedback/index.ts +0 -317
@@ -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';
@@ -127,6 +128,11 @@ export {
127
128
  VListItemDirective,
128
129
  VListSeparatorDirective,
129
130
  } from './directives';
131
+ import {
132
+ gateWanted,
133
+ injectGateDemandAbove,
134
+ type IGatedAccessibilityEvent,
135
+ } from '../../gate-demand';
130
136
  export type { IVListItemContext, IVListSeparatorContext } from './directives';
131
137
 
132
138
  // The Angular VirtualizedList prop surface. Mirrors React/Vue's IVirtualizedListProps MINUS the
@@ -210,6 +216,12 @@ interface IWindowCell<ItemT> {
210
216
  separatorContext?: IVListSeparatorContext<ItemT>;
211
217
  }
212
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.
213
225
  @Component({
214
226
  selector: 'VirtualizedList',
215
227
  standalone: true,
@@ -217,137 +229,149 @@ interface IWindowCell<ItemT> {
217
229
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
218
230
  ],
219
231
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
220
- imports: [ScrollView, RefreshControl, VListOutletDirective, ViewHost],
232
+ imports: [SymbioteHostPropsDirective, VListOutletDirective, ViewHost],
221
233
  changeDetection: ChangeDetectionStrategy.OnPush,
222
234
  template: `
223
- <ScrollView
224
- [horizontal]="isHorizontal"
225
- [style]="resolvedStyle"
226
- [contentContainerStyle]="resolvedContentContainerStyle"
227
- [testID]="foldedAccessibility().testID"
228
- [nativeID]="foldedAccessibility().nativeID"
229
- [accessible]="foldedAccessibility().accessible"
230
- [accessibilityLabel]="foldedAccessibility().accessibilityLabel"
231
- [accessibilityHint]="foldedAccessibility().accessibilityHint"
232
- [accessibilityRole]="foldedAccessibility().accessibilityRole"
233
- [accessibilityState]="foldedAccessibility().accessibilityState"
234
- [accessibilityValue]="foldedAccessibility().accessibilityValue"
235
- [accessibilityActions]="foldedAccessibility().accessibilityActions"
236
- [accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
237
- [importantForAccessibility]="
238
- foldedAccessibility().importantForAccessibility
239
- "
240
- [accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
241
- [screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
242
- [accessibilityViewIsModal]="
243
- foldedAccessibility().accessibilityViewIsModal
244
- "
245
- [accessibilityElementsHidden]="
246
- foldedAccessibility().accessibilityElementsHidden
247
- "
248
- [accessibilityIgnoresInvertColors]="
249
- foldedAccessibility().accessibilityIgnoresInvertColors
250
- "
251
- [accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
252
- [accessibilityRespondsToUserInteraction]="
253
- foldedAccessibility().accessibilityRespondsToUserInteraction
254
- "
255
- [accessibilityShowsLargeContentViewer]="
256
- foldedAccessibility().accessibilityShowsLargeContentViewer
257
- "
258
- [accessibilityLargeContentTitle]="
259
- foldedAccessibility().accessibilityLargeContentTitle
260
- "
261
- (accessibilityAction)="accessibilityActionTick($event)"
262
- (accessibilityTap)="accessibilityTapTick($event)"
263
- (magicTap)="magicTapTick($event)"
264
- (accessibilityEscape)="accessibilityEscapeTick($event)"
265
- [onScroll]="onScrollTick"
266
- (layout)="onLayoutTick($event)"
267
- [onScrollBeginDrag]="onScrollBeginDrag"
268
- [onScrollEndDrag]="onScrollEndDrag"
269
- [onMomentumScrollBegin]="onMomentumScrollBegin"
270
- [onMomentumScrollEnd]="onMomentumScrollEnd"
271
- [scrollEventThrottle]="scrollEventThrottle"
272
- [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
273
- [keyboardDismissMode]="keyboardDismissMode"
274
- [contentOffset]="commandedOffset"
275
- [stickyHeaderIndices]="renderedStickyIndices"
276
- [maintainVisibleContentPosition]="resolvedMaintainVisibleContentPosition"
277
- >
278
- @if (shouldRenderRefreshControl) {
279
- <RefreshControl
280
- [refreshing]="refreshing ?? false"
281
- (refresh)="handleRefresh()"
282
- [progressViewOffset]="progressViewOffset"
283
- />
284
- }
235
+ @if (isHorizontal) {
236
+ <horizontal-scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
237
+ @if (shouldRenderRefreshControl) {
238
+ <refresh-control
239
+ [symbioteHostProps]="refreshControlBag()"
240
+ ></refresh-control>
241
+ }
285
242
 
286
- @if (headerDir !== undefined) {
287
- <symbiote-view>
288
- <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
289
- </symbiote-view>
290
- }
243
+ @if (headerDir !== undefined) {
244
+ <view>
245
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
246
+ </view>
247
+ }
291
248
 
292
- @if (itemCount === 0) {
293
- @if (emptyDir !== undefined) {
294
- <symbiote-view>
295
- <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
296
- </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
+ }
297
293
  }
298
- } @else {
299
- @if (leadingSpacerStyle !== null) {
300
- <symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
294
+
295
+ @if (footerDir !== undefined) {
296
+ <view>
297
+ <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
298
+ </view>
301
299
  }
302
- @if (forcedStickyCell !== null) {
303
- <symbiote-view
304
- (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
305
- [style]="cellStyle"
306
- >
307
- <ng-container
308
- [vListOutlet]="itemDir?.templateRef"
309
- [vListOutletContext]="forcedStickyCell.context"
310
- ></ng-container>
311
- </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>
312
307
  }
313
- @if (gapSpacerStyle !== null) {
314
- <symbiote-view [style]="gapSpacerStyle"></symbiote-view>
308
+
309
+ @if (headerDir !== undefined) {
310
+ <view>
311
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
312
+ </view>
315
313
  }
316
- <!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
317
- (VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
318
- the chrome between two cells is gap + separator + gap while a spacer collapsing that
319
- region replaces it with a single gap — every cell below the leading spacer then lands
320
- short by (separator + gap) and the content jumps by that much whenever the window's
321
- first index moves. Measured at exactly 17px on device 2026-08-19; see
322
- .claude/rules/list-geometry-feedback-loop.md. -->
323
- @for (cell of windowCells; track cell.key) {
324
- <symbiote-view
325
- (layout)="handleCellLayout(cell.measure, $event)"
326
- [style]="cellStyle"
327
- >
328
- <ng-container
329
- [vListOutlet]="itemDir?.templateRef"
330
- [vListOutletContext]="cell.context"
331
- ></ng-container>
332
- @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
+ >
333
330
  <ng-container
334
- [vListOutlet]="separatorDir?.templateRef"
335
- [vListOutletContext]="cell.separatorContext"
331
+ [vListOutlet]="itemDir?.templateRef"
332
+ [vListOutletContext]="forcedStickyCell.context"
336
333
  ></ng-container>
337
- }
338
- </symbiote-view>
339
- }
340
- @if (trailingSpacerStyle !== null) {
341
- <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
+ }
342
366
  }
343
- }
344
367
 
345
- @if (footerDir !== undefined) {
346
- <symbiote-view>
347
- <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
348
- </symbiote-view>
349
- }
350
- </ScrollView>
368
+ @if (footerDir !== undefined) {
369
+ <view>
370
+ <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
371
+ </view>
372
+ }
373
+ </scroll-view>
374
+ }
351
375
  `,
352
376
  })
353
377
  export class VirtualizedList<ItemT = unknown>
@@ -361,7 +385,7 @@ export class VirtualizedList<ItemT = unknown>
361
385
  {
362
386
  // The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
363
387
  // `[onEndReached]="…"`. See handleRefresh/accessibility*Tick below for how the still-callback-
364
- // shaped ScrollView/RefreshControl @Input()s are fed from these.
388
+ // shaped scroll-view/refresh-control bag keys are fed from these.
365
389
  @Output() readonly endReached = new EventEmitter<{
366
390
  distanceFromEnd: number;
367
391
  }>();
@@ -381,6 +405,16 @@ export class VirtualizedList<ItemT = unknown>
381
405
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
382
406
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
383
407
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
408
+
409
+ // This wrapper binds the four gated accessibility events on the component it renders, which
410
+ // Angular forces to be unconditional and which would light that component's gates on every
411
+ // instance. It answers for them instead — see `gate-demand.ts`.
412
+ private readonly gateDemandAbove = injectGateDemandAbove();
413
+
414
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
415
+ return gateWanted(this.gateDemandAbove, name, this[name]);
416
+ }
417
+
384
418
  @Input({ required: true }) data!: unknown;
385
419
  @Input({ required: true }) getItem!: (data: unknown, index: number) => ItemT;
386
420
  @Input({ required: true }) getItemCount!: (data: unknown) => number;
@@ -475,10 +509,24 @@ export class VirtualizedList<ItemT = unknown>
475
509
  @ContentChild(VListSeparatorDirective)
476
510
  separatorDir?: VListSeparatorDirective<ItemT>;
477
511
 
478
- // The composed inner scroll view. Its instance IS an IScrollViewHandle (scrollTo / scrollToEnd /
479
- // flashScrollIndicators / getScrollNode), so the imperative handle delegates straight to it.
480
- // Available from ngAfterViewInit on; the handle reads the node lazily and no-ops before commit.
481
- @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
+ );
482
530
 
483
531
  // --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
484
532
  itemCount = EMPTY_OFFSET;
@@ -560,13 +608,13 @@ export class VirtualizedList<ItemT = unknown>
560
608
 
561
609
  private readonly cdr = inject(ChangeDetectorRef);
562
610
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
563
- // onto (see anchorHostStyle's doc comment) — NOT `scrollView` above, which targets the real
564
- // 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.
565
613
  private readonly elementRef = inject(ElementRef);
566
614
 
567
- // Bridges the non-reactive @Input fields into the reactive graph so the computed below can
568
- // memoize a bag derived from them (the same bridge ScrollView's shared.ts uses - read its comment
569
- // 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).
570
618
  //
571
619
  // ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
572
620
  // (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
@@ -575,12 +623,13 @@ export class VirtualizedList<ItemT = unknown>
575
623
  // instead of widening this one.
576
624
  private readonly inputsRevision = signal(0);
577
625
 
578
- // MEASURED: the template feeds 20 separate <ScrollView> inputs off this one bag, and Angular does
579
- // not cache a getter across binding expressions - as a getter it rebuilt the object 20 times per
580
- // refresh, and every consumer saw a fresh reference, so all 20 reported "changed" and wrote
581
- // through to the ScrollView. A computed evaluates once and hands back the SAME object until an
582
- // input actually changes. Every dependency below is an @Input, which is what makes
583
- // 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.
584
633
  readonly foldedAccessibility = computed<
585
634
  IAccessibilityProps & IAriaProps & Record<string, unknown>
586
635
  >(() => {
@@ -630,6 +679,57 @@ export class VirtualizedList<ItemT = unknown>
630
679
  return this.refreshRequested ?? this.refresh.observed;
631
680
  }
632
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
+
633
733
  get isHorizontal(): boolean {
634
734
  return this.horizontal === true;
635
735
  }
@@ -657,9 +757,10 @@ export class VirtualizedList<ItemT = unknown>
657
757
  return this.onStartReachedThreshold ?? DEFAULT_START_REACHED_THRESHOLD;
658
758
  }
659
759
 
660
- // ScrollView's onScroll stays an @Input() callback (an Animated.event(...) target must be able to
661
- // flow through it, which an @Output() can't carry), so this is a stable arrow field passed
662
- // 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.
663
764
  onScrollTick = (event: ISymbioteEvent): void => {
664
765
  countAngular('scrollTicks');
665
766
  const offset = readScrollOffset(event, this.isHorizontal);
@@ -679,9 +780,9 @@ export class VirtualizedList<ItemT = unknown>
679
780
  this.dispatch({ kind: 'layout', length });
680
781
  };
681
782
 
682
- // RefreshControl's refresh and ScrollView's accessibility events are real @Output()s now, bound
683
- // via (refresh)="handleRefresh()" / (accessibilityAction)="accessibilityActionTick($event)" etc.
684
- // — 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.
685
786
  handleRefresh = (): void => {
686
787
  this.refresh.emit();
687
788
  };
@@ -1245,23 +1346,23 @@ export class VirtualizedList<ItemT = unknown>
1245
1346
  }
1246
1347
 
1247
1348
  flashScrollIndicators(): void {
1248
- this.scrollView?.flashScrollIndicators();
1349
+ this.scrollHandle.flashScrollIndicators();
1249
1350
  }
1250
1351
 
1251
1352
  getNativeScrollRef(): IScrollViewHandle | null {
1252
- return this.scrollView ?? null;
1353
+ return this.scrollNode !== null ? this.scrollHandle : null;
1253
1354
  }
1254
1355
 
1255
1356
  getScrollableNode(): IScrollViewHandle | null {
1256
- return this.scrollView ?? null;
1357
+ return this.scrollNode !== null ? this.scrollHandle : null;
1257
1358
  }
1258
1359
 
1259
1360
  getScrollResponder(): IScrollViewHandle | null {
1260
- return this.scrollView ?? null;
1361
+ return this.scrollNode !== null ? this.scrollHandle : null;
1261
1362
  }
1262
1363
 
1263
1364
  getScrollNode(): ISymbioteNode | null {
1264
- return this.scrollView?.getScrollNode() ?? null;
1365
+ return this.scrollHandle.getScrollNode();
1265
1366
  }
1266
1367
 
1267
1368
  recordInteraction(): void {
@@ -1273,11 +1374,11 @@ export class VirtualizedList<ItemT = unknown>
1273
1374
  const target = this.isHorizontal
1274
1375
  ? { x: clamped, y: EMPTY_OFFSET }
1275
1376
  : { x: EMPTY_OFFSET, y: clamped };
1276
- if (this.scrollView !== undefined) {
1377
+ if (this.scrollNode !== null) {
1277
1378
  dlog(
1278
1379
  `Angular VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${this.isHorizontal})`,
1279
1380
  );
1280
- this.scrollView.scrollTo({ x: target.x, y: target.y, animated });
1381
+ this.scrollHandle.scrollTo({ x: target.x, y: target.y, animated });
1281
1382
  return;
1282
1383
  }
1283
1384
  dlog(`Angular VirtualizedList scrollTo offset=${clamped} pending-ref`);
@@ -88,6 +88,13 @@ export {
88
88
  VSectionItemDirective,
89
89
  VSectionSeparatorDirective,
90
90
  } from './directives';
91
+ import {
92
+ gateWanted,
93
+ injectGateDemandAbove,
94
+ provideGateDemand,
95
+ type IGateDemand,
96
+ type IGatedAccessibilityEvent,
97
+ } from '../../gate-demand';
91
98
  export type { IVSectionContext, IVSectionItemContext } from './directives';
92
99
 
93
100
  // The Angular VirtualizedSectionList prop surface. Mirrors React/Vue's IVirtualizedSectionListProps
@@ -160,6 +167,7 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
160
167
  @Component({
161
168
  selector: 'VirtualizedSectionList',
162
169
  standalone: true,
170
+ viewProviders: [provideGateDemand(() => VirtualizedSectionList)],
163
171
  hostDirectives: [
164
172
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
165
173
  ],
@@ -416,6 +424,16 @@ export class VirtualizedSectionList<ItemT = unknown>
416
424
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
417
425
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
418
426
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
427
+
428
+ // This wrapper binds the four gated accessibility events on the component it renders, which
429
+ // Angular forces to be unconditional and which would light that component's gates on every
430
+ // instance. It answers for them instead — see `gate-demand.ts`.
431
+ private readonly gateDemandAbove = injectGateDemandAbove();
432
+
433
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
434
+ return gateWanted(this.gateDemandAbove, name, this[name]);
435
+ }
436
+
419
437
  @Input() role?: IAriaProps['role'];
420
438
  @Input() ariaLabel?: string;
421
439
  @Input() ariaLabelledBy?: string;