@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,22 +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';
32
+ import { gateWanted, injectGateDemandAbove, } from '../../gate-demand';
34
33
  import * as i0 from "@angular/core";
35
34
  import * as i1 from "../../primitives/shared";
36
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.
37
42
  export class VirtualizedList {
38
43
  // The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
39
44
  // `[onEndReached]="…"`. See handleRefresh/accessibility*Tick below for how the still-callback-
40
- // shaped ScrollView/RefreshControl @Input()s are fed from these.
45
+ // shaped scroll-view/refresh-control bag keys are fed from these.
41
46
  endReached = new EventEmitter();
42
47
  startReached = new EventEmitter();
43
48
  refresh = new EventEmitter();
@@ -47,6 +52,13 @@ export class VirtualizedList {
47
52
  accessibilityTap = new EventEmitter();
48
53
  magicTap = new EventEmitter();
49
54
  accessibilityEscape = new EventEmitter();
55
+ // This wrapper binds the four gated accessibility events on the component it renders, which
56
+ // Angular forces to be unconditional and which would light that component's gates on every
57
+ // instance. It answers for them instead — see `gate-demand.ts`.
58
+ gateDemandAbove = injectGateDemandAbove();
59
+ wantsGate(name) {
60
+ return gateWanted(this.gateDemandAbove, name, this[name]);
61
+ }
50
62
  data;
51
63
  getItem;
52
64
  getItemCount;
@@ -129,10 +141,19 @@ export class VirtualizedList {
129
141
  footerDir;
130
142
  emptyDir;
131
143
  separatorDir;
132
- // The composed inner scroll view. Its instance IS an IScrollViewHandle (scrollTo / scrollToEnd /
133
- // flashScrollIndicators / getScrollNode), so the imperative handle delegates straight to it.
134
- // Available from ngAfterViewInit on; the handle reads the node lazily and no-ops before commit.
135
- 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);
136
157
  // --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
137
158
  itemCount = EMPTY_OFFSET;
138
159
  windowCells = [];
@@ -201,12 +222,12 @@ export class VirtualizedList {
201
222
  lastRecompute = undefined;
202
223
  cdr = inject(ChangeDetectorRef);
203
224
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
204
- // onto (see anchorHostStyle's doc comment) — NOT `scrollView` above, which targets the real
205
- // 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.
206
227
  elementRef = inject(ElementRef);
207
- // Bridges the non-reactive @Input fields into the reactive graph so the computed below can
208
- // memoize a bag derived from them (the same bridge ScrollView's shared.ts uses - read its comment
209
- // 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).
210
231
  //
211
232
  // ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
212
233
  // (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
@@ -215,12 +236,13 @@ export class VirtualizedList {
215
236
  // instead of widening this one.
216
237
  inputsRevision = signal(0, /* @ts-ignore */
217
238
  ...(ngDevMode ? [{ debugName: "inputsRevision" }] : /* istanbul ignore next */ []));
218
- // MEASURED: the template feeds 20 separate <ScrollView> inputs off this one bag, and Angular does
219
- // not cache a getter across binding expressions - as a getter it rebuilt the object 20 times per
220
- // refresh, and every consumer saw a fresh reference, so all 20 reported "changed" and wrote
221
- // through to the ScrollView. A computed evaluates once and hands back the SAME object until an
222
- // input actually changes. Every dependency below is an @Input, which is what makes
223
- // 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.
224
246
  foldedAccessibility = computed(() => {
225
247
  this.inputsRevision();
226
248
  return resolveAccessibilityProps({
@@ -265,6 +287,54 @@ export class VirtualizedList {
265
287
  get shouldRenderRefreshControl() {
266
288
  return this.refreshRequested ?? this.refresh.observed;
267
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
+ }
268
338
  get isHorizontal() {
269
339
  return this.horizontal === true;
270
340
  }
@@ -289,9 +359,10 @@ export class VirtualizedList {
289
359
  get onStartReachedThresholdValue() {
290
360
  return this.onStartReachedThreshold ?? DEFAULT_START_REACHED_THRESHOLD;
291
361
  }
292
- // ScrollView's onScroll stays an @Input() callback (an Animated.event(...) target must be able to
293
- // flow through it, which an @Output() can't carry), so this is a stable arrow field passed
294
- // 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.
295
366
  onScrollTick = (event) => {
296
367
  countAngular('scrollTicks');
297
368
  const offset = readScrollOffset(event, this.isHorizontal);
@@ -311,9 +382,9 @@ export class VirtualizedList {
311
382
  dlog(`Angular VirtualizedList onLayout viewport=${length}`);
312
383
  this.dispatch({ kind: 'layout', length });
313
384
  };
314
- // RefreshControl's refresh and ScrollView's accessibility events are real @Output()s now, bound
315
- // via (refresh)="handleRefresh()" / (accessibilityAction)="accessibilityActionTick($event)" etc.
316
- // — 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.
317
388
  handleRefresh = () => {
318
389
  this.refresh.emit();
319
390
  };
@@ -781,19 +852,19 @@ export class VirtualizedList {
781
852
  });
782
853
  }
783
854
  flashScrollIndicators() {
784
- this.scrollView?.flashScrollIndicators();
855
+ this.scrollHandle.flashScrollIndicators();
785
856
  }
786
857
  getNativeScrollRef() {
787
- return this.scrollView ?? null;
858
+ return this.scrollNode !== null ? this.scrollHandle : null;
788
859
  }
789
860
  getScrollableNode() {
790
- return this.scrollView ?? null;
861
+ return this.scrollNode !== null ? this.scrollHandle : null;
791
862
  }
792
863
  getScrollResponder() {
793
- return this.scrollView ?? null;
864
+ return this.scrollNode !== null ? this.scrollHandle : null;
794
865
  }
795
866
  getScrollNode() {
796
- return this.scrollView?.getScrollNode() ?? null;
867
+ return this.scrollHandle.getScrollNode();
797
868
  }
798
869
  recordInteraction() {
799
870
  this.dispatch({ kind: 'record-interaction' });
@@ -803,9 +874,9 @@ export class VirtualizedList {
803
874
  const target = this.isHorizontal
804
875
  ? { x: clamped, y: EMPTY_OFFSET }
805
876
  : { x: EMPTY_OFFSET, y: clamped };
806
- if (this.scrollView !== undefined) {
877
+ if (this.scrollNode !== null) {
807
878
  dlog(`Angular VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${this.isHorizontal})`);
808
- this.scrollView.scrollTo({ x: target.x, y: target.y, animated });
879
+ this.scrollHandle.scrollTo({ x: target.x, y: target.y, animated });
809
880
  return;
810
881
  }
811
882
  dlog(`Angular VirtualizedList scrollTo offset=${clamped} pending-ref`);
@@ -813,136 +884,148 @@ export class VirtualizedList {
813
884
  this.cdr.markForCheck();
814
885
  }
815
886
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VirtualizedList, deps: [], target: i0.ɵɵFactoryTarget.Component });
816
- 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: `
817
- <ScrollView
818
- [horizontal]="isHorizontal"
819
- [style]="resolvedStyle"
820
- [contentContainerStyle]="resolvedContentContainerStyle"
821
- [testID]="foldedAccessibility().testID"
822
- [nativeID]="foldedAccessibility().nativeID"
823
- [accessible]="foldedAccessibility().accessible"
824
- [accessibilityLabel]="foldedAccessibility().accessibilityLabel"
825
- [accessibilityHint]="foldedAccessibility().accessibilityHint"
826
- [accessibilityRole]="foldedAccessibility().accessibilityRole"
827
- [accessibilityState]="foldedAccessibility().accessibilityState"
828
- [accessibilityValue]="foldedAccessibility().accessibilityValue"
829
- [accessibilityActions]="foldedAccessibility().accessibilityActions"
830
- [accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
831
- [importantForAccessibility]="
832
- foldedAccessibility().importantForAccessibility
833
- "
834
- [accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
835
- [screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
836
- [accessibilityViewIsModal]="
837
- foldedAccessibility().accessibilityViewIsModal
838
- "
839
- [accessibilityElementsHidden]="
840
- foldedAccessibility().accessibilityElementsHidden
841
- "
842
- [accessibilityIgnoresInvertColors]="
843
- foldedAccessibility().accessibilityIgnoresInvertColors
844
- "
845
- [accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
846
- [accessibilityRespondsToUserInteraction]="
847
- foldedAccessibility().accessibilityRespondsToUserInteraction
848
- "
849
- [accessibilityShowsLargeContentViewer]="
850
- foldedAccessibility().accessibilityShowsLargeContentViewer
851
- "
852
- [accessibilityLargeContentTitle]="
853
- foldedAccessibility().accessibilityLargeContentTitle
854
- "
855
- (accessibilityAction)="accessibilityActionTick($event)"
856
- (accessibilityTap)="accessibilityTapTick($event)"
857
- (magicTap)="magicTapTick($event)"
858
- (accessibilityEscape)="accessibilityEscapeTick($event)"
859
- [onScroll]="onScrollTick"
860
- (layout)="onLayoutTick($event)"
861
- [onScrollBeginDrag]="onScrollBeginDrag"
862
- [onScrollEndDrag]="onScrollEndDrag"
863
- [onMomentumScrollBegin]="onMomentumScrollBegin"
864
- [onMomentumScrollEnd]="onMomentumScrollEnd"
865
- [scrollEventThrottle]="scrollEventThrottle"
866
- [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
867
- [keyboardDismissMode]="keyboardDismissMode"
868
- [contentOffset]="commandedOffset"
869
- [stickyHeaderIndices]="renderedStickyIndices"
870
- [maintainVisibleContentPosition]="resolvedMaintainVisibleContentPosition"
871
- >
872
- @if (shouldRenderRefreshControl) {
873
- <RefreshControl
874
- [refreshing]="refreshing ?? false"
875
- (refresh)="handleRefresh()"
876
- [progressViewOffset]="progressViewOffset"
877
- />
878
- }
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
+ }
879
895
 
880
- @if (headerDir !== undefined) {
881
- <symbiote-view>
882
- <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
883
- </symbiote-view>
884
- }
896
+ @if (headerDir !== undefined) {
897
+ <view>
898
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
899
+ </view>
900
+ }
885
901
 
886
- @if (itemCount === 0) {
887
- @if (emptyDir !== undefined) {
888
- <symbiote-view>
889
- <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
890
- </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
+ }
891
946
  }
892
- } @else {
893
- @if (leadingSpacerStyle !== null) {
894
- <symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
947
+
948
+ @if (footerDir !== undefined) {
949
+ <view>
950
+ <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
951
+ </view>
895
952
  }
896
- @if (forcedStickyCell !== null) {
897
- <symbiote-view
898
- (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
899
- [style]="cellStyle"
900
- >
901
- <ng-container
902
- [vListOutlet]="itemDir?.templateRef"
903
- [vListOutletContext]="forcedStickyCell.context"
904
- ></ng-container>
905
- </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>
906
960
  }
907
- @if (gapSpacerStyle !== null) {
908
- <symbiote-view [style]="gapSpacerStyle"></symbiote-view>
961
+
962
+ @if (headerDir !== undefined) {
963
+ <view>
964
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
965
+ </view>
909
966
  }
910
- <!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
911
- (VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
912
- the chrome between two cells is gap + separator + gap while a spacer collapsing that
913
- region replaces it with a single gap — every cell below the leading spacer then lands
914
- short by (separator + gap) and the content jumps by that much whenever the window's
915
- first index moves. Measured at exactly 17px on device 2026-08-19; see
916
- .claude/rules/list-geometry-feedback-loop.md. -->
917
- @for (cell of windowCells; track cell.key) {
918
- <symbiote-view
919
- (layout)="handleCellLayout(cell.measure, $event)"
920
- [style]="cellStyle"
921
- >
922
- <ng-container
923
- [vListOutlet]="itemDir?.templateRef"
924
- [vListOutletContext]="cell.context"
925
- ></ng-container>
926
- @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
+ >
927
983
  <ng-container
928
- [vListOutlet]="separatorDir?.templateRef"
929
- [vListOutletContext]="cell.separatorContext"
984
+ [vListOutlet]="itemDir?.templateRef"
985
+ [vListOutletContext]="forcedStickyCell.context"
930
986
  ></ng-container>
931
- }
932
- </symbiote-view>
933
- }
934
- @if (trailingSpacerStyle !== null) {
935
- <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
+ }
936
1019
  }
937
- }
938
1020
 
939
- @if (footerDir !== undefined) {
940
- <symbiote-view>
941
- <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
942
- </symbiote-view>
943
- }
944
- </ScrollView>
945
- `, 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" }], 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 });
946
1029
  }
947
1030
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VirtualizedList, decorators: [{
948
1031
  type: Component,
@@ -953,137 +1036,149 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
953
1036
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
954
1037
  ],
955
1038
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
956
- imports: [ScrollView, RefreshControl, VListOutletDirective, ViewHost],
1039
+ imports: [SymbioteHostPropsDirective, VListOutletDirective, ViewHost],
957
1040
  changeDetection: ChangeDetectionStrategy.OnPush,
958
1041
  template: `
959
- <ScrollView
960
- [horizontal]="isHorizontal"
961
- [style]="resolvedStyle"
962
- [contentContainerStyle]="resolvedContentContainerStyle"
963
- [testID]="foldedAccessibility().testID"
964
- [nativeID]="foldedAccessibility().nativeID"
965
- [accessible]="foldedAccessibility().accessible"
966
- [accessibilityLabel]="foldedAccessibility().accessibilityLabel"
967
- [accessibilityHint]="foldedAccessibility().accessibilityHint"
968
- [accessibilityRole]="foldedAccessibility().accessibilityRole"
969
- [accessibilityState]="foldedAccessibility().accessibilityState"
970
- [accessibilityValue]="foldedAccessibility().accessibilityValue"
971
- [accessibilityActions]="foldedAccessibility().accessibilityActions"
972
- [accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
973
- [importantForAccessibility]="
974
- foldedAccessibility().importantForAccessibility
975
- "
976
- [accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
977
- [screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
978
- [accessibilityViewIsModal]="
979
- foldedAccessibility().accessibilityViewIsModal
980
- "
981
- [accessibilityElementsHidden]="
982
- foldedAccessibility().accessibilityElementsHidden
983
- "
984
- [accessibilityIgnoresInvertColors]="
985
- foldedAccessibility().accessibilityIgnoresInvertColors
986
- "
987
- [accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
988
- [accessibilityRespondsToUserInteraction]="
989
- foldedAccessibility().accessibilityRespondsToUserInteraction
990
- "
991
- [accessibilityShowsLargeContentViewer]="
992
- foldedAccessibility().accessibilityShowsLargeContentViewer
993
- "
994
- [accessibilityLargeContentTitle]="
995
- foldedAccessibility().accessibilityLargeContentTitle
996
- "
997
- (accessibilityAction)="accessibilityActionTick($event)"
998
- (accessibilityTap)="accessibilityTapTick($event)"
999
- (magicTap)="magicTapTick($event)"
1000
- (accessibilityEscape)="accessibilityEscapeTick($event)"
1001
- [onScroll]="onScrollTick"
1002
- (layout)="onLayoutTick($event)"
1003
- [onScrollBeginDrag]="onScrollBeginDrag"
1004
- [onScrollEndDrag]="onScrollEndDrag"
1005
- [onMomentumScrollBegin]="onMomentumScrollBegin"
1006
- [onMomentumScrollEnd]="onMomentumScrollEnd"
1007
- [scrollEventThrottle]="scrollEventThrottle"
1008
- [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
1009
- [keyboardDismissMode]="keyboardDismissMode"
1010
- [contentOffset]="commandedOffset"
1011
- [stickyHeaderIndices]="renderedStickyIndices"
1012
- [maintainVisibleContentPosition]="resolvedMaintainVisibleContentPosition"
1013
- >
1014
- @if (shouldRenderRefreshControl) {
1015
- <RefreshControl
1016
- [refreshing]="refreshing ?? false"
1017
- (refresh)="handleRefresh()"
1018
- [progressViewOffset]="progressViewOffset"
1019
- />
1020
- }
1042
+ @if (isHorizontal) {
1043
+ <horizontal-scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
1044
+ @if (shouldRenderRefreshControl) {
1045
+ <refresh-control
1046
+ [symbioteHostProps]="refreshControlBag()"
1047
+ ></refresh-control>
1048
+ }
1021
1049
 
1022
- @if (headerDir !== undefined) {
1023
- <symbiote-view>
1024
- <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
1025
- </symbiote-view>
1026
- }
1050
+ @if (headerDir !== undefined) {
1051
+ <view>
1052
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
1053
+ </view>
1054
+ }
1027
1055
 
1028
- @if (itemCount === 0) {
1029
- @if (emptyDir !== undefined) {
1030
- <symbiote-view>
1031
- <ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
1032
- </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
+ }
1033
1100
  }
1034
- } @else {
1035
- @if (leadingSpacerStyle !== null) {
1036
- <symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
1101
+
1102
+ @if (footerDir !== undefined) {
1103
+ <view>
1104
+ <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
1105
+ </view>
1037
1106
  }
1038
- @if (forcedStickyCell !== null) {
1039
- <symbiote-view
1040
- (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
1041
- [style]="cellStyle"
1042
- >
1043
- <ng-container
1044
- [vListOutlet]="itemDir?.templateRef"
1045
- [vListOutletContext]="forcedStickyCell.context"
1046
- ></ng-container>
1047
- </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>
1048
1114
  }
1049
- @if (gapSpacerStyle !== null) {
1050
- <symbiote-view [style]="gapSpacerStyle"></symbiote-view>
1115
+
1116
+ @if (headerDir !== undefined) {
1117
+ <view>
1118
+ <ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
1119
+ </view>
1051
1120
  }
1052
- <!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
1053
- (VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
1054
- the chrome between two cells is gap + separator + gap while a spacer collapsing that
1055
- region replaces it with a single gap — every cell below the leading spacer then lands
1056
- short by (separator + gap) and the content jumps by that much whenever the window's
1057
- first index moves. Measured at exactly 17px on device 2026-08-19; see
1058
- .claude/rules/list-geometry-feedback-loop.md. -->
1059
- @for (cell of windowCells; track cell.key) {
1060
- <symbiote-view
1061
- (layout)="handleCellLayout(cell.measure, $event)"
1062
- [style]="cellStyle"
1063
- >
1064
- <ng-container
1065
- [vListOutlet]="itemDir?.templateRef"
1066
- [vListOutletContext]="cell.context"
1067
- ></ng-container>
1068
- @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
+ >
1069
1137
  <ng-container
1070
- [vListOutlet]="separatorDir?.templateRef"
1071
- [vListOutletContext]="cell.separatorContext"
1138
+ [vListOutlet]="itemDir?.templateRef"
1139
+ [vListOutletContext]="forcedStickyCell.context"
1072
1140
  ></ng-container>
1073
- }
1074
- </symbiote-view>
1075
- }
1076
- @if (trailingSpacerStyle !== null) {
1077
- <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
+ }
1078
1173
  }
1079
- }
1080
1174
 
1081
- @if (footerDir !== undefined) {
1082
- <symbiote-view>
1083
- <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
1084
- </symbiote-view>
1085
- }
1086
- </ScrollView>
1175
+ @if (footerDir !== undefined) {
1176
+ <view>
1177
+ <ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
1178
+ </view>
1179
+ }
1180
+ </scroll-view>
1181
+ }
1087
1182
  `,
1088
1183
  }]
1089
1184
  }], propDecorators: { endReached: [{
@@ -1256,8 +1351,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
1256
1351
  }], separatorDir: [{
1257
1352
  type: ContentChild,
1258
1353
  args: [VListSeparatorDirective]
1259
- }], scrollView: [{
1354
+ }], scrollHostRef: [{
1260
1355
  type: ViewChild,
1261
- args: [ScrollView]
1356
+ args: ['scrollHost', { read: ElementRef }]
1262
1357
  }] } });
1263
1358
  //# sourceMappingURL=index.js.map