@symbiote-native/angular 2.0.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +5 -4
  2. package/babel-register-composed.cjs +0 -8
  3. package/build/angular/callback-host.d.ts +25 -0
  4. package/build/angular/callback-host.js +211 -0
  5. package/build/angular/callback-host.js.map +1 -0
  6. package/build/angular/change-detection-flush.d.ts +14 -2
  7. package/build/angular/change-detection-flush.js +54 -6
  8. package/build/angular/change-detection-flush.js.map +1 -1
  9. package/build/angular/components/flat-list/index.js +39 -86
  10. package/build/angular/components/flat-list/index.js.map +1 -1
  11. package/build/angular/components/image-shared.js +49 -110
  12. package/build/angular/components/image-shared.js.map +1 -1
  13. package/build/angular/components/keyboard-avoiding-view/index.js +33 -72
  14. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  15. package/build/angular/components/modal/index.js +58 -103
  16. package/build/angular/components/modal/index.js.map +1 -1
  17. package/build/angular/components/pressable-props.d.ts +2 -0
  18. package/build/angular/components/section-list/index.js +20 -103
  19. package/build/angular/components/section-list/index.js.map +1 -1
  20. package/build/angular/components/touchable-props.d.ts +1 -0
  21. package/build/angular/components/virtualized-list/directives.js +32 -22
  22. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  23. package/build/angular/components/virtualized-list/index.d.ts +7 -1
  24. package/build/angular/components/virtualized-list/index.js +350 -359
  25. package/build/angular/components/virtualized-list/index.js.map +1 -1
  26. package/build/angular/components/virtualized-section-list/directives.js +20 -12
  27. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  28. package/build/angular/components/virtualized-section-list/index.js +36 -142
  29. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  30. package/build/angular/components.d.ts +1 -1
  31. package/build/angular/components.js +1 -1
  32. package/build/angular/components.js.map +1 -1
  33. package/build/angular/create-portal/index.js +9 -8
  34. package/build/angular/create-portal/index.js.map +1 -1
  35. package/build/angular/create-tunnel/index.js +13 -12
  36. package/build/angular/create-tunnel/index.js.map +1 -1
  37. package/build/angular/descriptor-to-angular/index.js +6 -6
  38. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  39. package/build/angular/diagnostics.d.ts +13 -0
  40. package/build/angular/diagnostics.js +18 -0
  41. package/build/angular/diagnostics.js.map +1 -1
  42. package/build/angular/element-props.d.ts +1 -3
  43. package/build/angular/elements.d.ts +46 -32
  44. package/build/angular/elements.js +217 -434
  45. package/build/angular/elements.js.map +1 -1
  46. package/build/angular/index.d.ts +3 -1
  47. package/build/angular/index.js +44 -13
  48. package/build/angular/index.js.map +1 -1
  49. package/build/angular/modules/animated/animated-leaf-binder.js +0 -2
  50. package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
  51. package/build/angular/modules/animated/create-animated-component.js +69 -74
  52. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  53. package/build/angular/modules/animated/index.d.ts +30 -19
  54. package/build/angular/modules/status-bar/index.js +2 -8
  55. package/build/angular/modules/status-bar/index.js.map +1 -1
  56. package/build/angular/primitives/index.d.ts +14 -28
  57. package/build/angular/primitives/index.js +44 -107
  58. package/build/angular/primitives/index.js.map +1 -1
  59. package/build/angular/primitives/shared.d.ts +1 -0
  60. package/build/angular/primitives/shared.js +47 -32
  61. package/build/angular/primitives/shared.js.map +1 -1
  62. package/build/angular/render/index.js +40 -3
  63. package/build/angular/render/index.js.map +1 -1
  64. package/build/angular/renderer/index.d.ts +80 -0
  65. package/build/angular/renderer/index.js +324 -114
  66. package/build/angular/renderer/index.js.map +1 -1
  67. package/build/angular/runtime-matching.d.ts +9 -0
  68. package/build/angular/runtime-matching.js +82 -0
  69. package/build/angular/runtime-matching.js.map +1 -0
  70. package/build/angular/services/color-scheme.service.js +4 -4
  71. package/build/angular/services/color-scheme.service.js.map +1 -1
  72. package/build/angular/services/window-dimensions.service.js +4 -4
  73. package/build/angular/services/window-dimensions.service.js.map +1 -1
  74. package/build/angular/style-host.d.ts +11 -0
  75. package/build/angular/style-host.js +86 -0
  76. package/build/angular/style-host.js.map +1 -0
  77. package/package.json +6 -6
  78. package/src/callback-host.ts +229 -0
  79. package/src/change-detection-flush.ts +62 -9
  80. package/src/components/flat-list/index.ts +2 -1
  81. package/src/components/keyboard-avoiding-view/index.ts +8 -0
  82. package/src/components/pressable-props.ts +8 -0
  83. package/src/components/touchable-props.ts +4 -0
  84. package/src/components/virtualized-list/index.ts +98 -45
  85. package/src/components.ts +0 -2
  86. package/src/diagnostics.ts +18 -0
  87. package/src/element-props.ts +1 -5
  88. package/src/elements.ts +159 -38
  89. package/src/index.ts +47 -14
  90. package/src/primitives/index.ts +14 -70
  91. package/src/primitives/shared.ts +21 -6
  92. package/src/render/index.ts +56 -2
  93. package/src/renderer/index.ts +378 -104
  94. package/src/runtime-matching.ts +80 -0
  95. package/src/style-host.ts +94 -0
  96. package/tsconfig.angular.base.json +11 -0
@@ -58,7 +58,23 @@ interface IViewMarker {
58
58
  markForCheck(): void;
59
59
  }
60
60
 
61
- type ICallbackWrapper = (key: string, value: unknown) => unknown;
61
+ export type ICallbackWrapper = (key: string, value: unknown) => unknown;
62
+
63
+ /**
64
+ * Is this prop write one the wrapper would touch at all?
65
+ *
66
+ * Exported so a CALLER can ask before building a wrapper it may never need — an element directive is
67
+ * constructed per TAG, and a screen's tags overwhelmingly carry no `on*` function prop. Keeping the
68
+ * question here rather than copying the two tests at the call site is what stops the predicate and
69
+ * the wrapper drifting apart.
70
+ */
71
+ export function isWrappableCallback(key: string, value: unknown): boolean {
72
+ return (
73
+ ON_PREFIX.test(key) &&
74
+ typeof value === 'function' &&
75
+ key !== PER_FRAME_CALLBACK
76
+ );
77
+ }
62
78
 
63
79
  /**
64
80
  * Makes an app callback handed to the engine as a PROP re-enter Angular's update loop.
@@ -82,14 +98,18 @@ type ICallbackWrapper = (key: string, value: unknown) => unknown;
82
98
  * ONE wrapper per original handler, for the caller's lifetime. A fresh closure per push would make
83
99
  * the engine store a new listener every time and would defeat every downstream identity check.
84
100
  */
85
- export function createCallbackWrapper(
86
- view: IViewMarker,
87
- node: unknown,
88
- ): ICallbackWrapper {
89
- const wrappers = new WeakMap<object, (...args: unknown[]) => unknown>();
101
+ export function createCallbackWrapper(node: unknown): ICallbackWrapper {
102
+ // Allocated on the FIRST wrap, not at construction. The map is per-CALLER, so an eager one is one
103
+ // `WeakMap` per directive instance — 7 000 on a 1 000-row create of the benchmark row, none of
104
+ // which is ever read, because that row carries no `on*` prop.
105
+ let wrappers: WeakMap<object, (...args: unknown[]) => unknown> | undefined;
90
106
  return (key: string, value: unknown): unknown => {
91
- if (!ON_PREFIX.test(key) || typeof value !== 'function') return value;
92
- if (key === PER_FRAME_CALLBACK) return value;
107
+ // The second test is redundant at RUNTIME and load-bearing for the TYPE: `isWrappableCallback`
108
+ // answers a boolean, which narrows nothing, and the alternative is a cast. Keeping the `typeof`
109
+ // here is what lets `value` be a `Function` below without one.
110
+ if (!isWrappableCallback(key, value) || typeof value !== 'function')
111
+ return value;
112
+ wrappers ??= new WeakMap();
93
113
  const cached = wrappers.get(value);
94
114
  if (cached !== undefined) return cached;
95
115
  // Reflect.apply rather than a cast-to-signature local: `typeof value === 'function'` narrows to
@@ -97,7 +117,7 @@ export function createCallbackWrapper(
97
117
  // `undefined` as the receiver preserves the previous unbound call.
98
118
  const wrapper = (...args: unknown[]): unknown => {
99
119
  const result: unknown = Reflect.apply(value, undefined, args);
100
- view.markForCheck();
120
+ markViewFor(node);
101
121
  flushViewFor(node);
102
122
  return result;
103
123
  };
@@ -106,6 +126,39 @@ export function createCallbackWrapper(
106
126
  };
107
127
  }
108
128
 
129
+ /**
130
+ * The MARKER half, keyed on the node rather than injected.
131
+ *
132
+ * `markForCheck` needs the view holding the binding, and until 2026-09-18 the only way to have one
133
+ * was for `SymbioteElement` to inject a `ChangeDetectorRef` — on EVERY element, so that its lazy
134
+ * `on*` wrapper could reach one on the few that carry a callback. An injection is ~1-2.4 us per
135
+ * element on JavaScriptCore (`core/engine/cpp/tests/js/angular-directive-cost.itest.ts`), so a
136
+ * thousand-row screen paid for ten thousand `ViewRef`s to serve none.
137
+ *
138
+ * `SymbioteCallbackHost` registers here instead, and it MATCHES on the callback attributes rather
139
+ * than being injected — so an element with no `on*` binding neither instantiates it nor pays for it.
140
+ * Measured as the whole trade, selector included: -3.9 / -9.3 / -25.5 ms over ten thousand elements.
141
+ *
142
+ * Separate from `viewFlushes` deliberately. They come from different directives, on overlapping but
143
+ * different sets of tags, and they are not the same instruction: this one SCHEDULES and reaches every
144
+ * ancestor, that one re-checks ONE view synchronously. Collapsing them into a single entry would make
145
+ * whichever directive registered last decide which of the two an element gets.
146
+ */
147
+ const viewMarkers = new WeakMap<object, IViewMarker>();
148
+
149
+ export function registerViewMarker(node: object, marker: IViewMarker): void {
150
+ viewMarkers.set(node, marker);
151
+ }
152
+
153
+ export function unregisterViewMarker(node: object): void {
154
+ viewMarkers.delete(node);
155
+ }
156
+
157
+ export function markViewFor(node: unknown): void {
158
+ if (typeof node !== 'object' || node === null) return;
159
+ viewMarkers.get(node)?.markForCheck();
160
+ }
161
+
109
162
  export function registerViewFlush(node: object, flush: () => void): void {
110
163
  viewFlushes.set(node, flush);
111
164
  }
@@ -516,7 +516,8 @@ export class FlatList<ItemT = unknown>
516
516
  // ---- multi-column data adaptation (the virtualized stream is rows) ----
517
517
  getRow = (_data: unknown, index: number): IRow<ItemT> => this.rows[index];
518
518
  getRowCount = (_data: unknown): number => this.rows.length;
519
- rowKey = (row: IRow<ItemT>, _index: number): string => rowKeyExtractor(row);
519
+ rowKey = (row: IRow<ItemT>, _index: number): string =>
520
+ rowKeyExtractor(row, this.keyExtractor);
520
521
 
521
522
  // Viewability over rows expands back to per-item tokens, so the caller sees item-level
522
523
  // visibility, not row-level (shared expandRowViewability), matching Vue.
@@ -26,7 +26,9 @@ import {
26
26
  } from '@angular/core';
27
27
  import {
28
28
  computeInset,
29
+ configureKeyboardAvoidingAnimation,
29
30
  keyboardAvoidingEventNamesFor,
31
+ readKeyboardAnimationTiming,
30
32
  readKeyboardFrame,
31
33
  readLayoutFrame,
32
34
  readPrefersCrossFadeTransitions,
@@ -227,6 +229,12 @@ export class KeyboardAvoidingView
227
229
  previousInset: this.inset,
228
230
  prefersCrossFadeTransitions: this.prefersCrossFadeTransitions,
229
231
  });
232
+ // RN's `_updateBottomIfNecessary` skips the animation when the inset did not change.
233
+ if (next !== this.inset)
234
+ configureKeyboardAvoidingAnimation(
235
+ readKeyboardAnimationTiming(payload),
236
+ this.enabled !== false,
237
+ );
230
238
  dlog(`KeyboardAvoidingView show -> inset ${next}`);
231
239
  this.inset = next;
232
240
  this.changeDetector.markForCheck();
@@ -33,7 +33,15 @@ export interface IAngularPressableProps
33
33
  delayLongPress?: number;
34
34
  disabled?: boolean;
35
35
  cancelable?: boolean;
36
+ // Tells native to stand down once this Pressable claims the gesture (RN Pressable.js:123,
37
+ // Pressability.js onResponderGrant), so a parent ScrollView cannot steal it mid-drag. Default
38
+ // false.
39
+ blockNativeResponder?: boolean;
36
40
  hitSlop?: IRectOffset;
41
+ // RN's snapshot affordance (`Pressable.js:151`, `TouchableHighlight.js:61`): render the control in
42
+ // its pressed state with no gesture, so a test can capture it. Consumed by the engine and stripped
43
+ // before the payload — no ViewConfig declares it.
44
+ testOnly_pressed?: boolean;
37
45
  pressRetentionOffset?: IRectOffset;
38
46
  unstable_pressDelay?: number;
39
47
  android_ripple?: IPressableAndroidRippleConfig;
@@ -27,4 +27,8 @@ export type IAngularTouchableOpacityProps = IAngularTouchableBaseProps & {
27
27
  export type IAngularTouchableHighlightProps = IAngularTouchableBaseProps & {
28
28
  activeOpacity?: number;
29
29
  underlayColor?: string;
30
+ // RN's snapshot affordance (`Pressable.js:151`, `TouchableHighlight.js:61`): render the control in
31
+ // its pressed state with no gesture, so a test can capture it. Consumed by the engine and stripped
32
+ // before the payload — no ViewConfig declares it.
33
+ testOnly_pressed?: boolean;
30
34
  };
@@ -44,10 +44,8 @@ import {
44
44
  type OnDestroy,
45
45
  } from '@angular/core';
46
46
  import {
47
- DEFAULT_END_REACHED_THRESHOLD,
48
47
  DEFAULT_INITIAL_NUM_TO_RENDER,
49
48
  DEFAULT_MAX_TO_RENDER_PER_BATCH,
50
- DEFAULT_START_REACHED_THRESHOLD,
51
49
  DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
52
50
  DEFAULT_WINDOW_SIZE,
53
51
  EMPTY_OFFSET,
@@ -214,6 +212,13 @@ interface IWindowCell<ItemT> {
214
212
  context: IVListItemContext<ItemT>;
215
213
  measure: (event: ISymbioteEvent) => void;
216
214
  separatorContext?: IVListSeparatorContext<ItemT>;
215
+ /**
216
+ * Picks the cell's TAG in the template — `sticky-header` pins, `view` does not.
217
+ *
218
+ * It is stable per cell key (an index is a section header or it is not), so the `@if` never
219
+ * swaps an element type under a live cell and nothing remounts as the window slides.
220
+ */
221
+ isSticky: boolean;
217
222
  }
218
223
 
219
224
  // The scroll body is identical on both axes; only the outer tag differs (the axis comes from the
@@ -257,7 +262,7 @@ interface IWindowCell<ItemT> {
257
262
  <view [style]="leadingSpacerStyle"></view>
258
263
  }
259
264
  @if (forcedStickyCell !== null) {
260
- <view
265
+ <sticky-header
261
266
  (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
262
267
  [style]="cellStyle"
263
268
  >
@@ -265,27 +270,45 @@ interface IWindowCell<ItemT> {
265
270
  [vListOutlet]="itemDir?.templateRef"
266
271
  [vListOutletContext]="forcedStickyCell.context"
267
272
  ></ng-container>
268
- </view>
273
+ </sticky-header>
269
274
  }
270
275
  @if (gapSpacerStyle !== null) {
271
276
  <view [style]="gapSpacerStyle"></view>
272
277
  }
273
278
  @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) {
279
+ @if (cell.isSticky) {
280
+ <sticky-header
281
+ (layout)="handleCellLayout(cell.measure, $event)"
282
+ [style]="cellStyle"
283
+ >
283
284
  <ng-container
284
- [vListOutlet]="separatorDir?.templateRef"
285
- [vListOutletContext]="cell.separatorContext"
285
+ [vListOutlet]="itemDir?.templateRef"
286
+ [vListOutletContext]="cell.context"
286
287
  ></ng-container>
287
- }
288
- </view>
288
+ @if (cell.separatorContext !== undefined) {
289
+ <ng-container
290
+ [vListOutlet]="separatorDir?.templateRef"
291
+ [vListOutletContext]="cell.separatorContext"
292
+ ></ng-container>
293
+ }
294
+ </sticky-header>
295
+ } @else {
296
+ <view
297
+ (layout)="handleCellLayout(cell.measure, $event)"
298
+ [style]="cellStyle"
299
+ >
300
+ <ng-container
301
+ [vListOutlet]="itemDir?.templateRef"
302
+ [vListOutletContext]="cell.context"
303
+ ></ng-container>
304
+ @if (cell.separatorContext !== undefined) {
305
+ <ng-container
306
+ [vListOutlet]="separatorDir?.templateRef"
307
+ [vListOutletContext]="cell.separatorContext"
308
+ ></ng-container>
309
+ }
310
+ </view>
311
+ }
289
312
  }
290
313
  @if (trailingSpacerStyle !== null) {
291
314
  <view [style]="trailingSpacerStyle"></view>
@@ -323,7 +346,7 @@ interface IWindowCell<ItemT> {
323
346
  <view [style]="leadingSpacerStyle"></view>
324
347
  }
325
348
  @if (forcedStickyCell !== null) {
326
- <view
349
+ <sticky-header
327
350
  (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
328
351
  [style]="cellStyle"
329
352
  >
@@ -331,7 +354,7 @@ interface IWindowCell<ItemT> {
331
354
  [vListOutlet]="itemDir?.templateRef"
332
355
  [vListOutletContext]="forcedStickyCell.context"
333
356
  ></ng-container>
334
- </view>
357
+ </sticky-header>
335
358
  }
336
359
  @if (gapSpacerStyle !== null) {
337
360
  <view [style]="gapSpacerStyle"></view>
@@ -344,21 +367,39 @@ interface IWindowCell<ItemT> {
344
367
  first index moves. Measured at exactly 17px on device 2026-08-19; see
345
368
  .claude/rules/list-geometry-feedback-loop.md. -->
346
369
  @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) {
370
+ @if (cell.isSticky) {
371
+ <sticky-header
372
+ (layout)="handleCellLayout(cell.measure, $event)"
373
+ [style]="cellStyle"
374
+ >
356
375
  <ng-container
357
- [vListOutlet]="separatorDir?.templateRef"
358
- [vListOutletContext]="cell.separatorContext"
376
+ [vListOutlet]="itemDir?.templateRef"
377
+ [vListOutletContext]="cell.context"
359
378
  ></ng-container>
360
- }
361
- </view>
379
+ @if (cell.separatorContext !== undefined) {
380
+ <ng-container
381
+ [vListOutlet]="separatorDir?.templateRef"
382
+ [vListOutletContext]="cell.separatorContext"
383
+ ></ng-container>
384
+ }
385
+ </sticky-header>
386
+ } @else {
387
+ <view
388
+ (layout)="handleCellLayout(cell.measure, $event)"
389
+ [style]="cellStyle"
390
+ >
391
+ <ng-container
392
+ [vListOutlet]="itemDir?.templateRef"
393
+ [vListOutletContext]="cell.context"
394
+ ></ng-container>
395
+ @if (cell.separatorContext !== undefined) {
396
+ <ng-container
397
+ [vListOutlet]="separatorDir?.templateRef"
398
+ [vListOutletContext]="cell.separatorContext"
399
+ ></ng-container>
400
+ }
401
+ </view>
402
+ }
362
403
  }
363
404
  @if (trailingSpacerStyle !== null) {
364
405
  <view [style]="trailingSpacerStyle"></view>
@@ -540,7 +581,6 @@ export class VirtualizedList<ItemT = unknown>
540
581
  gapSpacerStyle: IViewStyle | null = null;
541
582
  trailingSpacerStyle: IViewStyle | null = null;
542
583
  cellStyle: IViewStyle | undefined = undefined;
543
- renderedStickyIndices: number[] | undefined = undefined;
544
584
  // Bound to the template's `[style]="resolvedStyle"`, which Angular compiles to the built-in
545
585
  // ɵɵstyleMap instruction — it only understands a flat object, never an array (RN's own
546
586
  // `style={[a, b]}` composition idiom crashes deep inside Angular's styling engine), so this
@@ -700,7 +740,10 @@ export class VirtualizedList<ItemT = unknown>
700
740
  keyboardShouldPersistTaps: this.keyboardShouldPersistTaps,
701
741
  keyboardDismissMode: this.keyboardDismissMode,
702
742
  contentOffset: this.commandedOffset,
703
- stickyHeaderIndices: this.renderedStickyIndices,
743
+ // `stickyHeaderIndices` is deliberately NOT forwarded — it numbers the scroll view's PAINT
744
+ // children, and a windowed list paints a header, a spacer and a slice, so the positions move
745
+ // every time the window slides and the behavior re-wraps a different child each pass. The
746
+ // cells carry the `sticky-header` TAG instead, which pins by DOCUMENT order.
704
747
  maintainVisibleContentPosition:
705
748
  this.resolvedMaintainVisibleContentPosition,
706
749
  // The four boolean-gated Fabric events (`.claude/rules/fabric-boolean-event-gates.md`): the
@@ -750,11 +793,11 @@ export class VirtualizedList<ItemT = unknown>
750
793
  this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD
751
794
  );
752
795
  }
753
- private get onEndReachedThresholdValue(): number {
754
- return this.onEndReachedThreshold ?? DEFAULT_END_REACHED_THRESHOLD;
796
+ private get onEndReachedThresholdValue(): number | undefined {
797
+ return this.onEndReachedThreshold;
755
798
  }
756
- private get onStartReachedThresholdValue(): number {
757
- return this.onStartReachedThreshold ?? DEFAULT_START_REACHED_THRESHOLD;
799
+ private get onStartReachedThresholdValue(): number | undefined {
800
+ return this.onStartReachedThreshold;
758
801
  }
759
802
 
760
803
  // onScroll stays a plain callback bag key (an Animated.event(...) target must be able to flow
@@ -933,7 +976,12 @@ export class VirtualizedList<ItemT = unknown>
933
976
  const info = effect.info;
934
977
  const map = effect.map;
935
978
  const fire = (): void => {
936
- for (const pair of pairs) pair.onViewableItemsChanged(info);
979
+ for (const pair of pairs) {
980
+ pair.onViewableItemsChanged({
981
+ ...info,
982
+ viewabilityConfig: pair.viewabilityConfig,
983
+ });
984
+ }
937
985
  this.dispatch({ kind: 'viewable-fired', map });
938
986
  };
939
987
  if (this.viewableTimer !== null) {
@@ -1084,7 +1132,6 @@ export class VirtualizedList<ItemT = unknown>
1084
1132
  this.leadingSpacerStyle = null;
1085
1133
  this.gapSpacerStyle = null;
1086
1134
  this.trailingSpacerStyle = null;
1087
- this.renderedStickyIndices = undefined;
1088
1135
  dlog(
1089
1136
  `Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`,
1090
1137
  );
@@ -1112,10 +1159,7 @@ export class VirtualizedList<ItemT = unknown>
1112
1159
  plan.trailingExtent > EMPTY_OFFSET
1113
1160
  ? this.spacerStyle(plan.trailingExtent)
1114
1161
  : null;
1115
- this.renderedStickyIndices =
1116
- stickySet !== undefined && plan.stickyChildPositions.length > 0
1117
- ? plan.stickyChildPositions
1118
- : undefined;
1162
+ // `plan.stickyChildPositions` is deliberately NOT read — it was the input to the index form.
1119
1163
  dlog(
1120
1164
  `STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
1121
1165
  `childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
@@ -1129,6 +1173,8 @@ export class VirtualizedList<ItemT = unknown>
1129
1173
  plan.forcedStickyCell.index,
1130
1174
  plan.forcedStickyCell.key,
1131
1175
  false,
1176
+ // Sticky by construction — `forcedStickyCell` exists only for an index in `stickySet`.
1177
+ true,
1132
1178
  )
1133
1179
  : null;
1134
1180
 
@@ -1141,7 +1187,12 @@ export class VirtualizedList<ItemT = unknown>
1141
1187
  // 2026-08-19 as a run of cells all shifting by exactly the divider's 1px.
1142
1188
  const includeSeparator = hasSeparators && planned.index < m.count - 1;
1143
1189
  cells.push(
1144
- this.buildWindowCell(planned.index, planned.key, includeSeparator),
1190
+ this.buildWindowCell(
1191
+ planned.index,
1192
+ planned.key,
1193
+ includeSeparator,
1194
+ stickySet?.has(planned.index) === true,
1195
+ ),
1145
1196
  );
1146
1197
  }
1147
1198
  this.windowCells = cells;
@@ -1162,6 +1213,7 @@ export class VirtualizedList<ItemT = unknown>
1162
1213
  index: number,
1163
1214
  key: string,
1164
1215
  includeSeparator: boolean,
1216
+ isSticky: boolean,
1165
1217
  ): IWindowCell<ItemT> {
1166
1218
  const item = this.getItem(this.data, index);
1167
1219
  return {
@@ -1172,6 +1224,7 @@ export class VirtualizedList<ItemT = unknown>
1172
1224
  separatorContext: includeSeparator
1173
1225
  ? this.buildSeparatorContext(index, item)
1174
1226
  : undefined,
1227
+ isSticky,
1175
1228
  };
1176
1229
  }
1177
1230
 
package/src/components.ts CHANGED
@@ -179,8 +179,6 @@ export {
179
179
  HorizontalScrollContentView,
180
180
  TextInputHost,
181
181
  MultilineTextInputHost,
182
- ManagedTextInputHost,
183
- ManagedMultilineTextInputHost,
184
182
  SwitchHost,
185
183
  ActivityIndicatorHost,
186
184
  SafeAreaViewHost,
@@ -116,6 +116,24 @@ export function noteAngularWrite(propName: string): void {
116
116
  writesByProp.set(propName, (writesByProp.get(propName) ?? 0) + 1);
117
117
  }
118
118
 
119
+ /**
120
+ * A style KEY write, named `style.<key>` in the readout — and the prefix is built HERE.
121
+ *
122
+ * `noteAngularWrite` gates itself, but an ARGUMENT is evaluated before the callee can refuse it, so
123
+ * `noteAngularWrite(`style.${key}`)` builds a string on every `setStyle` in a Release build with the
124
+ * detail map off. `setStyle` is Angular's only styling channel — one call per key per node — so the
125
+ * bench row's create allocated ~7 000 of them for nobody.
126
+ *
127
+ * It is the same trap `CLAUDE.md` records for `dlog` arguments on this very renderer, in a different
128
+ * function; `tests/dlog-argument-budget.test.ts` scans for the `dlog` spelling and does not see this
129
+ * one.
130
+ */
131
+ export function noteAngularStyleWrite(key: string): void {
132
+ if (!isDetailEnabled) return;
133
+ const name = `style.${key}`;
134
+ writesByProp.set(name, (writesByProp.get(name) ?? 0) + 1);
135
+ }
136
+
119
137
  export function noteAngularCreate(tagName: string): void {
120
138
  if (!isDetailEnabled) return;
121
139
  createsByTag.set(tagName, (createsByTag.get(tagName) ?? 0) + 1);
@@ -15,8 +15,6 @@
15
15
  // wrapper component accepted. The claim that a directive can reclaim it "solely by executing a
16
16
  // linked AOT artifact" was answered rather than assumed: `elements.test.ts` compiles the binding
17
17
  // through real ngtsc and runs the LINKED output. See `SymbioteElement.style`.
18
- //
19
- // `symbioteStyle` stays as an alias — it is public API and the renderer still folds it.
20
18
  import type {
21
19
  IAccessibilityProps,
22
20
  IAriaProps,
@@ -45,10 +43,8 @@ export interface IElementProps
45
43
  renderToHardwareTextureAndroid?: boolean;
46
44
  shouldRasterizeIOS?: boolean;
47
45
  needsOffscreenAlphaCompositing?: boolean;
48
- /** The array-capable `[style]`, aliased to `style` in the renderer. */
49
- symbioteStyle?: IStyleProp<IViewStyle>;
50
46
  /**
51
- * RN's own spelling. The press-state callback is in the union because a subclass cannot widen an
47
+ * The press-state callback is in the union because a subclass cannot widen an
52
48
  * inherited property and `<pressable>`/`<touchable-*>` are the tags that take one; on every other
53
49
  * tag the engine resolves it at `pressed: false`.
54
50
  */