@symbiote-native/angular 0.6.2 → 0.7.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 (162) hide show
  1. package/bin/symbiote-angular-dev.cjs +20 -19
  2. package/build/angular/anchor-host-registry.js +2 -2
  3. package/build/angular/anchor-host-registry.js.map +1 -1
  4. package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
  5. package/build/angular/components/activity-indicator/index.android.js +6 -3
  6. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  7. package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
  8. package/build/angular/components/activity-indicator/index.ios.js +6 -3
  9. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  10. package/build/angular/components/activity-indicator/shared.js +3 -3
  11. package/build/angular/components/button.d.ts +4 -2
  12. package/build/angular/components/button.js +28 -17
  13. package/build/angular/components/button.js.map +1 -1
  14. package/build/angular/components/flat-list/index.d.ts +5 -3
  15. package/build/angular/components/flat-list/index.js +35 -38
  16. package/build/angular/components/flat-list/index.js.map +1 -1
  17. package/build/angular/components/image/index.android.d.ts +8 -3
  18. package/build/angular/components/image/index.android.js +39 -9
  19. package/build/angular/components/image/index.android.js.map +1 -1
  20. package/build/angular/components/image/index.ios.d.ts +8 -3
  21. package/build/angular/components/image/index.ios.js +39 -9
  22. package/build/angular/components/image/index.ios.js.map +1 -1
  23. package/build/angular/components/image/shared.d.ts +4 -1
  24. package/build/angular/components/image/shared.js +33 -2
  25. package/build/angular/components/image/shared.js.map +1 -1
  26. package/build/angular/components/image-background.d.ts +2 -1
  27. package/build/angular/components/image-background.js +6 -4
  28. package/build/angular/components/image-background.js.map +1 -1
  29. package/build/angular/components/input-accessory-view/index.d.ts +9 -4
  30. package/build/angular/components/input-accessory-view/index.js +43 -9
  31. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  32. package/build/angular/components/keyboard-avoiding-view/index.d.ts +2 -1
  33. package/build/angular/components/keyboard-avoiding-view/index.js +6 -4
  34. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  35. package/build/angular/components/modal/index.d.ts +11 -8
  36. package/build/angular/components/modal/index.js +45 -28
  37. package/build/angular/components/modal/index.js.map +1 -1
  38. package/build/angular/components/pressable/index.d.ts +11 -5
  39. package/build/angular/components/pressable/index.js +61 -12
  40. package/build/angular/components/pressable/index.js.map +1 -1
  41. package/build/angular/components/refresh-control/index.d.ts +8 -4
  42. package/build/angular/components/refresh-control/index.js +52 -16
  43. package/build/angular/components/refresh-control/index.js.map +1 -1
  44. package/build/angular/components/safe-area-view/index.d.ts +9 -4
  45. package/build/angular/components/safe-area-view/index.js +46 -9
  46. package/build/angular/components/safe-area-view/index.js.map +1 -1
  47. package/build/angular/components/scroll-view/index.android.d.ts +2 -3
  48. package/build/angular/components/scroll-view/index.android.js +49 -52
  49. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  50. package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
  51. package/build/angular/components/scroll-view/index.ios.js +15 -17
  52. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  53. package/build/angular/components/scroll-view/projection.d.ts +3 -0
  54. package/build/angular/components/scroll-view/projection.js +68 -7
  55. package/build/angular/components/scroll-view/projection.js.map +1 -1
  56. package/build/angular/components/scroll-view/shared.d.ts +14 -5
  57. package/build/angular/components/scroll-view/shared.js +198 -69
  58. package/build/angular/components/scroll-view/shared.js.map +1 -1
  59. package/build/angular/components/scroll-view/sticky-header.js +14 -19
  60. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  61. package/build/angular/components/section-list/index.d.ts +2 -1
  62. package/build/angular/components/section-list/index.js +31 -39
  63. package/build/angular/components/section-list/index.js.map +1 -1
  64. package/build/angular/components/switch/index.android.d.ts +7 -3
  65. package/build/angular/components/switch/index.android.js +38 -10
  66. package/build/angular/components/switch/index.android.js.map +1 -1
  67. package/build/angular/components/switch/index.ios.d.ts +7 -3
  68. package/build/angular/components/switch/index.ios.js +38 -10
  69. package/build/angular/components/switch/index.ios.js.map +1 -1
  70. package/build/angular/components/switch/shared.d.ts +12 -3
  71. package/build/angular/components/switch/shared.js +66 -5
  72. package/build/angular/components/switch/shared.js.map +1 -1
  73. package/build/angular/components/text-input.d.ts +10 -2
  74. package/build/angular/components/text-input.js +46 -6
  75. package/build/angular/components/text-input.js.map +1 -1
  76. package/build/angular/components/touchable/index.d.ts +17 -8
  77. package/build/angular/components/touchable/index.js +78 -19
  78. package/build/angular/components/touchable/index.js.map +1 -1
  79. package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
  80. package/build/angular/components/touchable-native-feedback/index.js +61 -25
  81. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  82. package/build/angular/components/virtualized-list/directives.js +18 -18
  83. package/build/angular/components/virtualized-list/index.d.ts +12 -4
  84. package/build/angular/components/virtualized-list/index.js +163 -68
  85. package/build/angular/components/virtualized-list/index.js.map +1 -1
  86. package/build/angular/components/virtualized-section-list/directives.js +12 -12
  87. package/build/angular/components/virtualized-section-list/index.d.ts +2 -1
  88. package/build/angular/components/virtualized-section-list/index.js +6 -4
  89. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  90. package/build/angular/components.d.ts +1 -1
  91. package/build/angular/components.js +5 -0
  92. package/build/angular/components.js.map +1 -1
  93. package/build/angular/create-portal/index.js +29 -35
  94. package/build/angular/create-portal/index.js.map +1 -1
  95. package/build/angular/create-tunnel/index.js +19 -22
  96. package/build/angular/create-tunnel/index.js.map +1 -1
  97. package/build/angular/descriptor-to-angular/index.js +3 -3
  98. package/build/angular/index.d.ts +4 -2
  99. package/build/angular/index.js +2 -2
  100. package/build/angular/index.js.map +1 -1
  101. package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
  102. package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
  103. package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
  104. package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
  105. package/build/angular/modules/animated/create-animated-component.js +83 -50
  106. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  107. package/build/angular/modules/app-registry/index.js.map +1 -1
  108. package/build/angular/modules/status-bar/index.js +3 -3
  109. package/build/angular/primitives/index.d.ts +1 -1
  110. package/build/angular/primitives/index.js +46 -46
  111. package/build/angular/primitives/index.js.map +1 -1
  112. package/build/angular/primitives/shared.d.ts +39 -1
  113. package/build/angular/primitives/shared.js +110 -9
  114. package/build/angular/primitives/shared.js.map +1 -1
  115. package/build/angular/render/index.js +23 -35
  116. package/build/angular/render/index.js.map +1 -1
  117. package/build/angular/services/color-scheme.service.js +3 -3
  118. package/build/angular/services/window-dimensions.service.js +3 -3
  119. package/package.json +9 -7
  120. package/src/anchor-host-registry.ts +2 -2
  121. package/src/components/activity-indicator/index.android.ts +2 -0
  122. package/src/components/activity-indicator/index.ios.ts +2 -0
  123. package/src/components/button.ts +28 -14
  124. package/src/components/flat-list/index.ts +34 -36
  125. package/src/components/image/index.android.ts +50 -6
  126. package/src/components/image/index.ios.ts +50 -6
  127. package/src/components/image/shared.ts +34 -2
  128. package/src/components/image-background.ts +2 -1
  129. package/src/components/input-accessory-view/index.ts +42 -4
  130. package/src/components/keyboard-avoiding-view/index.ts +7 -1
  131. package/src/components/modal/index.ts +51 -26
  132. package/src/components/pressable/index.ts +67 -8
  133. package/src/components/refresh-control/index.ts +54 -12
  134. package/src/components/safe-area-view/index.ts +50 -5
  135. package/src/components/scroll-view/index.android.ts +34 -41
  136. package/src/components/scroll-view/index.ios.ts +10 -21
  137. package/src/components/scroll-view/projection.ts +79 -7
  138. package/src/components/scroll-view/shared.ts +223 -68
  139. package/src/components/scroll-view/sticky-header.ts +11 -16
  140. package/src/components/section-list/index.ts +27 -36
  141. package/src/components/switch/index.android.ts +48 -7
  142. package/src/components/switch/index.ios.ts +48 -7
  143. package/src/components/switch/shared.ts +79 -3
  144. package/src/components/text-input.ts +52 -2
  145. package/src/components/touchable/index.ts +76 -16
  146. package/src/components/touchable-native-feedback/index.ts +62 -21
  147. package/src/components/virtualized-list/index.ts +137 -44
  148. package/src/components/virtualized-section-list/index.ts +2 -1
  149. package/src/components.ts +5 -0
  150. package/src/create-portal/index.ts +23 -29
  151. package/src/create-tunnel/index.ts +13 -16
  152. package/src/index.ts +30 -1
  153. package/src/modules/animated/animated-leaf-binder.ts +28 -77
  154. package/src/modules/animated/create-animated-component.ts +75 -36
  155. package/src/modules/app-registry/index.ts +6 -8
  156. package/src/primitives/index.ts +1 -0
  157. package/src/primitives/shared.ts +102 -3
  158. package/src/render/index.ts +23 -35
  159. package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
  160. package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
  161. package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
  162. package/src/modules/animated/animated-leaf-binder/index.ts +0 -101
@@ -1,22 +1,19 @@
1
1
  // TouchableNativeFeedback: the Angular lifecycle half. Android's ripple / state-drawable
2
- // touchable, built on Pressable like the rest of the family (the Angular twin of the React/Vue
3
- // adapter). RN realizes its feedback by cloning the child into an RCTView carrying native ripple
4
- // props; we instead nest the child under a feedback <symbiote-view> that carries those props
5
- // (nativeBackgroundAndroid / nativeForegroundAndroid), inside a <Pressable> that owns the press
6
- // wiring. The native props are read by Android's ReactViewManager; on iOS they are inert props,
7
- // so the child still renders with working press wiring. The static factories + background mapping
8
- // are shared in @symbiote-native/components and reused verbatim Angular only attaches them onto the
9
- // component class and nests the feedback view. No JS-side platform branch (one Fabric path both
10
- // platforms), so this stays a flat single file, mirroring React/Vue.
2
+ // touchable, built on Pressable like the rest of the family. RN realizes its feedback by cloning
3
+ // the child into an RCTView carrying native ripple props; we instead nest the child under a
4
+ // feedback <symbiote-view> that carries those props (nativeBackgroundAndroid /
5
+ // nativeForegroundAndroid), inside a <Pressable> that owns the press wiring. The native props are
6
+ // read by Android's ReactViewManager; on iOS they are inert, so the child still renders with
7
+ // working press wiring. The static factories + background mapping are shared in
8
+ // @symbiote-native/components and reused verbatim. One Fabric path both platforms, so this stays
9
+ // a flat single file, mirroring React/Vue.
11
10
  //
12
11
  // This component's own press/hover events are real @Output() EventEmitters too, matching
13
- // Pressable (which it wraps) — `(press)="press.emit($event)"`, never `[onPress]="onPress"`.
14
- // Non-event config (delayLongPress, hitSlop, ...) still rides down as plain @Input() bindings. The
15
- // a11y identity bag + the four a11y events live on the feedback view — the one host intrinsic here —
16
- // folded through the shared resolveAccessibilityProps and routed through emit() on the (event)
17
- // channel (Angular blocks [onX] property bindings on host elements; events flow through (event)
18
- // only). disabled folds into the a11y state via resolveDisabledAccessibilityState, exactly as
19
- // Pressable does for its own host.
12
+ // Pressable (which it wraps) — `(press)="press.emit($event)"`, never `[onPress]="onPress"`. The
13
+ // a11y identity bag + the four a11y events live on the feedback view — the one host intrinsic here
14
+ // folded through the shared resolveAccessibilityProps and routed through emit() on the (event)
15
+ // channel (Angular blocks [onX] property bindings on host elements). disabled folds into the a11y
16
+ // state via resolveDisabledAccessibilityState, exactly as Pressable does for its own host.
20
17
 
21
18
  import {
22
19
  CUSTOM_ELEMENTS_SCHEMA,
@@ -24,9 +21,12 @@ import {
24
21
  Component,
25
22
  ElementRef,
26
23
  EventEmitter,
24
+ computed,
27
25
  inject,
28
26
  Input,
29
27
  Output,
28
+ signal,
29
+ type DoCheck,
30
30
  type OnChanges,
31
31
  } from '@angular/core';
32
32
  import {
@@ -90,7 +90,7 @@ export type IAngularTouchableNativeFeedbackProps = Omit<IAngularPressableInputs,
90
90
  [android_disableSound]="android_disableSound"
91
91
  >
92
92
  <symbiote-view
93
- [symbioteHostProps]="hostProps"
93
+ [symbioteHostProps]="hostProps()"
94
94
  (accessibilityAction)="emit(accessibilityAction, $event)"
95
95
  (accessibilityTap)="emit(accessibilityTap, $event)"
96
96
  (magicTap)="emit(magicTap, $event)"
@@ -101,7 +101,9 @@ export type IAngularTouchableNativeFeedbackProps = Omit<IAngularPressableInputs,
101
101
  </Pressable>
102
102
  `,
103
103
  })
104
- export class TouchableNativeFeedback implements IAngularTouchableNativeFeedbackProps, OnChanges {
104
+ export class TouchableNativeFeedback
105
+ implements IAngularTouchableNativeFeedbackProps, OnChanges, DoCheck
106
+ {
105
107
  // The static helpers are pure config-dict producers; they live as members on the component class
106
108
  // so callers reach them as `TouchableNativeFeedback.Ripple(...)`, exactly like RN.
107
109
  static readonly SelectableBackground = selectableBackground;
@@ -175,7 +177,37 @@ export class TouchableNativeFeedback implements IAngularTouchableNativeFeedbackP
175
177
  // anchor's class-derived style is the ONLY style source for hostProps.style below.
176
178
  private readonly elementRef = inject(ElementRef);
177
179
 
180
+ // Bridges the non-reactive @Input fields above into the reactive graph, so the computed() bag
181
+ // below can memoize a value derived from them. Signal inputs would make this unnecessary, but
182
+ // `input()` is visible only to the AOT compiler and this package's unit suite runs on JIT - see
183
+ // the `angular-adapter-change-detection` skill, §6. Mirrors ScrollView's identical bridge
184
+ // (components/scroll-view/shared.ts), including its constraint: ONLY safe for a bag whose every
185
+ // dependency is an @Input or carries its own signal, never for plain internal mutable state.
186
+ private readonly inputsRevision = signal(0);
187
+
188
+ // The anchor's class-derived style is hostProps' one dependency that is NOT an @Input:
189
+ // `class="..."`/`[class.x]`/`[ngClass]` at the use site resolves through the renderer's
190
+ // addClass/removeClass onto the anchor node (anchorHostStyle's doc comment), so it never appears
191
+ // in SimpleChanges and ngOnChanges cannot cover it. The engine's commitClassStyle allocates a
192
+ // fresh [classStyle, explicitStyle] array on exactly the passes where a class token actually
193
+ // moved, so signal.set's own Object.is check makes an unchanged poll a no-op that dirties nothing.
194
+ private readonly anchorStyle = signal<unknown>(undefined);
195
+
196
+ // ngDoCheck is the only hook that observes the anchor AFTER the parent wrote it. Angular flushes
197
+ // a node's pre-order hooks at the next `advance` PAST it (hooks.ts callHooks runs "until that
198
+ // node index exclusive") or in refreshView's post-template flush - both strictly after the
199
+ // parent's classProp on this element, and strictly before this component's own view refreshes.
200
+ // Angular also nulls the active consumer around lifecycle hooks (render3/hooks.ts), so this
201
+ // write registers no dependency on the caller's view and cannot throw NG0600.
202
+ ngDoCheck(): void {
203
+ this.anchorStyle.set(anchorHostStyle(this.elementRef));
204
+ }
205
+
178
206
  ngOnChanges(): void {
207
+ // Tells the computed bag below that an @Input moved - plain class fields are not reactive on
208
+ // their own. Must stay in ngOnChanges: that is the single moment Angular has finished writing
209
+ // every changed input for this pass.
210
+ this.inputsRevision.update(revision => revision + 1);
179
211
  dlog(
180
212
  `TouchableNativeFeedback render ${this.resolvedBackground.type} useForeground ${this.useForeground === true}`,
181
213
  );
@@ -195,17 +227,26 @@ export class TouchableNativeFeedback implements IAngularTouchableNativeFeedbackP
195
227
 
196
228
  // Assembles the SAME resolved values the template used to bind one-by-one into a single
197
229
  // flat bag for `[symbioteHostProps]` (Renderer2.setProperty per key) — see primitives/shared.ts.
198
- get hostProps(): Record<string, unknown> {
230
+ //
231
+ // MEASURED 2026-08-16: as a getter this rebuilt the bag on every refresh of this view, and a
232
+ // press dirties this very view (SymbioteHostPropsDirective wraps each flat-bag onX with
233
+ // markForCheck), so `[symbioteHostProps]`'s reference check failed every time and re-pushed
234
+ // every key through Renderer2.setProperty -> routeProp for an identical bag. Memoized, an
235
+ // unchanged bag costs one reference compare. Every dependency below is either an @Input (covered
236
+ // by inputsRevision) or the anchor style (covered by its own signal) - nothing here reads plain
237
+ // internal mutable state, which is what makes memoizing this one safe.
238
+ readonly hostProps = computed<Record<string, unknown>>(() => {
239
+ this.inputsRevision();
199
240
  return {
200
241
  nativeBackgroundAndroid: this.feedback['nativeBackgroundAndroid'],
201
242
  nativeForegroundAndroid: this.feedback['nativeForegroundAndroid'],
202
- style: anchorHostStyle(this.elementRef),
243
+ style: this.anchorStyle(),
203
244
  testID: this.testID,
204
245
  nativeID: this.nativeID,
205
246
  accessible: this.accessible,
206
247
  ...this.folded,
207
248
  };
208
- }
249
+ });
209
250
 
210
251
  // Forward an engine event to the matching @Output(), narrowing the template's untyped $event.
211
252
  emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
@@ -35,9 +35,12 @@ import {
35
35
  Input,
36
36
  Output,
37
37
  ViewChild,
38
+ computed,
38
39
  inject,
40
+ signal,
39
41
  type AfterViewChecked,
40
42
  type DoCheck,
43
+ type OnChanges,
41
44
  type OnDestroy,
42
45
  } from '@angular/core';
43
46
  import {
@@ -86,7 +89,7 @@ import {
86
89
  } from '@symbiote-native/engine';
87
90
  import { ScrollView } from '../scroll-view';
88
91
  import { RefreshControl } from '../refresh-control';
89
- import { anchorHostStyle, ViewHost } from '../../primitives';
92
+ import { anchorHostStyle, SymbioteStyleInputDirective, ViewHost } from '../../primitives';
90
93
  import {
91
94
  VListEmptyDirective,
92
95
  VListFooterDirective,
@@ -202,6 +205,7 @@ interface IWindowCell<ItemT> {
202
205
  @Component({
203
206
  selector: 'VirtualizedList',
204
207
  standalone: true,
208
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
205
209
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
206
210
  imports: [ScrollView, RefreshControl, VListOutletDirective, ViewHost],
207
211
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -210,30 +214,30 @@ interface IWindowCell<ItemT> {
210
214
  [horizontal]="isHorizontal"
211
215
  [style]="resolvedStyle"
212
216
  [contentContainerStyle]="resolvedContentContainerStyle"
213
- [testID]="foldedAccessibility.testID"
214
- [nativeID]="foldedAccessibility.nativeID"
215
- [accessible]="foldedAccessibility.accessible"
216
- [accessibilityLabel]="foldedAccessibility.accessibilityLabel"
217
- [accessibilityHint]="foldedAccessibility.accessibilityHint"
218
- [accessibilityRole]="foldedAccessibility.accessibilityRole"
219
- [accessibilityState]="foldedAccessibility.accessibilityState"
220
- [accessibilityValue]="foldedAccessibility.accessibilityValue"
221
- [accessibilityActions]="foldedAccessibility.accessibilityActions"
222
- [accessibilityLabelledBy]="foldedAccessibility.accessibilityLabelledBy"
223
- [importantForAccessibility]="foldedAccessibility.importantForAccessibility"
224
- [accessibilityLiveRegion]="foldedAccessibility.accessibilityLiveRegion"
225
- [screenReaderFocusable]="foldedAccessibility.screenReaderFocusable"
226
- [accessibilityViewIsModal]="foldedAccessibility.accessibilityViewIsModal"
227
- [accessibilityElementsHidden]="foldedAccessibility.accessibilityElementsHidden"
228
- [accessibilityIgnoresInvertColors]="foldedAccessibility.accessibilityIgnoresInvertColors"
229
- [accessibilityLanguage]="foldedAccessibility.accessibilityLanguage"
217
+ [testID]="foldedAccessibility().testID"
218
+ [nativeID]="foldedAccessibility().nativeID"
219
+ [accessible]="foldedAccessibility().accessible"
220
+ [accessibilityLabel]="foldedAccessibility().accessibilityLabel"
221
+ [accessibilityHint]="foldedAccessibility().accessibilityHint"
222
+ [accessibilityRole]="foldedAccessibility().accessibilityRole"
223
+ [accessibilityState]="foldedAccessibility().accessibilityState"
224
+ [accessibilityValue]="foldedAccessibility().accessibilityValue"
225
+ [accessibilityActions]="foldedAccessibility().accessibilityActions"
226
+ [accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
227
+ [importantForAccessibility]="foldedAccessibility().importantForAccessibility"
228
+ [accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
229
+ [screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
230
+ [accessibilityViewIsModal]="foldedAccessibility().accessibilityViewIsModal"
231
+ [accessibilityElementsHidden]="foldedAccessibility().accessibilityElementsHidden"
232
+ [accessibilityIgnoresInvertColors]="foldedAccessibility().accessibilityIgnoresInvertColors"
233
+ [accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
230
234
  [accessibilityRespondsToUserInteraction]="
231
- foldedAccessibility.accessibilityRespondsToUserInteraction
235
+ foldedAccessibility().accessibilityRespondsToUserInteraction
232
236
  "
233
237
  [accessibilityShowsLargeContentViewer]="
234
- foldedAccessibility.accessibilityShowsLargeContentViewer
238
+ foldedAccessibility().accessibilityShowsLargeContentViewer
235
239
  "
236
- [accessibilityLargeContentTitle]="foldedAccessibility.accessibilityLargeContentTitle"
240
+ [accessibilityLargeContentTitle]="foldedAccessibility().accessibilityLargeContentTitle"
237
241
  (accessibilityAction)="accessibilityActionTick($event)"
238
242
  (accessibilityTap)="accessibilityTapTick($event)"
239
243
  (magicTap)="magicTapTick($event)"
@@ -275,6 +279,20 @@ interface IWindowCell<ItemT> {
275
279
  @if (leadingSpacerStyle !== null) {
276
280
  <symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
277
281
  }
282
+ @if (forcedStickyCell !== null) {
283
+ <symbiote-view
284
+ (layout)="handleCellLayout(forcedStickyCell.measure, $event)"
285
+ [style]="cellStyle"
286
+ >
287
+ <ng-container
288
+ [vListOutlet]="itemDir?.templateRef"
289
+ [vListOutletContext]="forcedStickyCell.context"
290
+ ></ng-container>
291
+ </symbiote-view>
292
+ }
293
+ @if (gapSpacerStyle !== null) {
294
+ <symbiote-view [style]="gapSpacerStyle"></symbiote-view>
295
+ }
278
296
  @for (cell of windowCells; track cell.key) {
279
297
  <symbiote-view (layout)="handleCellLayout(cell.measure, $event)" [style]="cellStyle">
280
298
  <ng-container
@@ -310,6 +328,7 @@ export class VirtualizedList<ItemT = unknown>
310
328
  IVirtualizedListHandle,
311
329
  DoCheck,
312
330
  AfterViewChecked,
331
+ OnChanges,
313
332
  OnDestroy
314
333
  {
315
334
  // The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
@@ -426,7 +445,13 @@ export class VirtualizedList<ItemT = unknown>
426
445
  // --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
427
446
  itemCount = EMPTY_OFFSET;
428
447
  windowCells: IWindowCell<ItemT>[] = [];
448
+ // The nearest sticky-header cell force-mounted outside [first,last] (plan.forcedStickyCell), the
449
+ // Angular twin of RN's _ensureClosestStickyHeader. Rendered ahead of windowCells, right after the
450
+ // leading spacer, with gapSpacerStyle filling the gap to the window's own first cell — see
451
+ // buildListPlan's doc comment for the exact child order this assumes.
452
+ forcedStickyCell: IWindowCell<ItemT> | null = null;
429
453
  leadingSpacerStyle: IViewStyle | null = null;
454
+ gapSpacerStyle: IViewStyle | null = null;
430
455
  trailingSpacerStyle: IViewStyle | null = null;
431
456
  cellStyle: IViewStyle | undefined = undefined;
432
457
  renderedStickyIndices: number[] | undefined = undefined;
@@ -457,6 +482,9 @@ export class VirtualizedList<ItemT = unknown>
457
482
  private batchTimer: ReturnType<typeof setTimeout> | null = null;
458
483
  // Dedupes the after-commit effects: they run only when the windowing signature changed.
459
484
  private lastEffectSignature = '';
485
+ // Dedupes dispatch's markForCheck against the RENDERED window (see dispatch below). Distinct from
486
+ // lastEffectSignature, which gates the batch-fill timer and deliberately includes scrollOffset.
487
+ private lastRenderSignature = '';
460
488
  // Dedupes ngDoCheck's own recompute. WITHOUT this, recomputeView() unconditionally rebuilds
461
489
  // windowCells (and each cell's `separators` handle — fresh closures every call) on EVERY CD pass,
462
490
  // including ones triggered by something else entirely in the app. A fresh context object flows
@@ -476,7 +504,27 @@ export class VirtualizedList<ItemT = unknown>
476
504
  // inner `<ScrollView>` one level down (itself its own separate anchor host).
477
505
  private readonly elementRef = inject(ElementRef);
478
506
 
479
- get foldedAccessibility(): IAccessibilityProps & IAriaProps & Record<string, unknown> {
507
+ // Bridges the non-reactive @Input fields into the reactive graph so the computed below can
508
+ // memoize a bag derived from them (the same bridge ScrollView's shared.ts uses - read its comment
509
+ // for why signal inputs are not an option while this package's unit suite runs on JIT).
510
+ //
511
+ // ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
512
+ // (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
513
+ // callbacks that never touch ngOnChanges, so nothing derived from it may ride this signal - it
514
+ // would memoize a window that stops re-computing mid-scroll. Make that state its own signal
515
+ // instead of widening this one.
516
+ private readonly inputsRevision = signal(0);
517
+
518
+ // MEASURED: the template feeds 20 separate <ScrollView> inputs off this one bag, and Angular does
519
+ // not cache a getter across binding expressions - as a getter it rebuilt the object 20 times per
520
+ // refresh, and every consumer saw a fresh reference, so all 20 reported "changed" and wrote
521
+ // through to the ScrollView. A computed evaluates once and hands back the SAME object until an
522
+ // input actually changes. Every dependency below is an @Input, which is what makes
523
+ // inputsRevision a complete dependency set here.
524
+ readonly foldedAccessibility = computed<
525
+ IAccessibilityProps & IAriaProps & Record<string, unknown>
526
+ >(() => {
527
+ this.inputsRevision();
480
528
  return resolveAccessibilityProps({
481
529
  testID: this.testID,
482
530
  nativeID: this.nativeID,
@@ -514,7 +562,7 @@ export class VirtualizedList<ItemT = unknown>
514
562
  'aria-valuenow': this.ariaValueNow,
515
563
  'aria-valuetext': this.ariaValueText,
516
564
  });
517
- }
565
+ });
518
566
 
519
567
  get shouldRenderRefreshControl(): boolean {
520
568
  return this.refreshRequested ?? this.refresh.observed;
@@ -631,10 +679,22 @@ export class VirtualizedList<ItemT = unknown>
631
679
  const inputs = this.buildInputs();
632
680
  const result = reduceList(this.listState, action, inputs);
633
681
  this.runEffects(result.effects, inputs);
634
- if (result.changed) {
635
- this.renderVersion += 1;
636
- this.cdr.markForCheck();
637
- }
682
+ if (!result.changed) return;
683
+ this.renderVersion += 1;
684
+ // The reducer reports `changed` for EVERY scroll offset - it has to, since the offset feeds
685
+ // end-reached distance, viewability and the batch-fill timer, all handled above in runEffects.
686
+ // The TEMPLATE reads none of those; it reads the window. Marking on the offset repainted the
687
+ // whole ancestor chain 60 times a second on a sticky screen for a window that had not moved.
688
+ const signature = this.renderSignature();
689
+ if (signature === this.lastRenderSignature) return;
690
+ this.lastRenderSignature = signature;
691
+ this.cdr.markForCheck();
692
+ }
693
+
694
+ // The window, not the offset: exactly the metrics recomputeView turns into cells and spacers.
695
+ private renderSignature(): string {
696
+ const m = this.listState.metrics;
697
+ return `${m.first}|${m.last}|${m.count}|${m.total}`;
638
698
  }
639
699
 
640
700
  private runEffects(effects: IListEffect<ItemT>[], inputs: IListReducerInputs<ItemT>): void {
@@ -690,6 +750,14 @@ export class VirtualizedList<ItemT = unknown>
690
750
  }
691
751
  }
692
752
 
753
+ // foldedAccessibility reads plain @Input fields, which are NOT reactive on their own - this bump
754
+ // is what tells that computed an input changed. It must stay in ngOnChanges: that is the single
755
+ // moment Angular has finished writing every changed input for this pass, and it runs before
756
+ // ngDoCheck below, so the recompute already sees the current values.
757
+ ngOnChanges(): void {
758
+ this.inputsRevision.update(revision => revision + 1);
759
+ }
760
+
693
761
  // Once-per-CD recompute, BEFORE the template bindings are read (so the freshly computed
694
762
  // template-bound fields render this pass — computing them in ngAfterContentChecked instead would
695
763
  // trip ExpressionChangedAfterItHasBeenChecked). The dedup guard runs refresh-metrics (which owns
@@ -710,6 +778,10 @@ export class VirtualizedList<ItemT = unknown>
710
778
  this.stickyHeaderIndices,
711
779
  this.maintainVisibleContentPosition,
712
780
  this.style,
781
+ // `recomputeView` folds this into the committed style, but it arrives through
782
+ // addClass/removeClass, never as an @Input - so without it here no entry moves on a class
783
+ // toggle and the gate skips the recompute forever. Reference-stable, so the dedup holds.
784
+ anchorHostStyle(this.elementRef),
713
785
  this.contentContainerStyle,
714
786
  // Folds scroll / layout / measure / batch-tick: dispatch bumps renderVersion on any change.
715
787
  this.renderVersion,
@@ -779,7 +851,9 @@ export class VirtualizedList<ItemT = unknown>
779
851
 
780
852
  if (m.count === FIRST_INDEX) {
781
853
  this.windowCells = [];
854
+ this.forcedStickyCell = null;
782
855
  this.leadingSpacerStyle = null;
856
+ this.gapSpacerStyle = null;
783
857
  this.trailingSpacerStyle = null;
784
858
  this.renderedStickyIndices = undefined;
785
859
  dlog(`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`);
@@ -800,32 +874,29 @@ export class VirtualizedList<ItemT = unknown>
800
874
  });
801
875
  this.leadingSpacerStyle =
802
876
  plan.leadingExtent > EMPTY_OFFSET ? this.spacerStyle(plan.leadingExtent) : null;
877
+ this.gapSpacerStyle = plan.gapExtent > EMPTY_OFFSET ? this.spacerStyle(plan.gapExtent) : null;
803
878
  this.trailingSpacerStyle =
804
879
  plan.trailingExtent > EMPTY_OFFSET ? this.spacerStyle(plan.trailingExtent) : null;
805
880
  this.renderedStickyIndices =
806
881
  stickySet !== undefined && plan.stickyChildPositions.length > 0
807
882
  ? plan.stickyChildPositions
808
883
  : undefined;
884
+ dlog(
885
+ `STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
886
+ `childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
887
+ `forcedStickyCell=${plan.forcedStickyCell === undefined ? 'none' : plan.forcedStickyCell.index} ` +
888
+ `window=[${m.first},${m.last}] hasHeader=${this.headerDir !== undefined}`,
889
+ );
890
+
891
+ this.forcedStickyCell =
892
+ plan.forcedStickyCell !== undefined
893
+ ? this.buildWindowCell(plan.forcedStickyCell.index, plan.forcedStickyCell.key, false)
894
+ : null;
809
895
 
810
896
  const cells: IWindowCell<ItemT>[] = [];
811
897
  for (const planned of plan.cells) {
812
- const item = this.getItem(this.data, planned.index);
813
- const context: IVListItemContext<ItemT> = {
814
- $implicit: item,
815
- index: planned.index,
816
- separators: this.makeSeparators(planned.index),
817
- };
818
- let separatorContext: IVListSeparatorContext<ItemT> | undefined;
819
- if (hasSeparators && planned.index < m.last) {
820
- separatorContext = this.buildSeparatorContext(planned.index, item);
821
- }
822
- cells.push({
823
- key: planned.key,
824
- index: planned.index,
825
- context,
826
- measure: this.cellMeasure(planned.index),
827
- separatorContext,
828
- });
898
+ const includeSeparator = hasSeparators && planned.index < m.last;
899
+ cells.push(this.buildWindowCell(planned.index, planned.key, includeSeparator));
829
900
  }
830
901
  this.windowCells = cells;
831
902
 
@@ -835,6 +906,28 @@ export class VirtualizedList<ItemT = unknown>
835
906
  );
836
907
  }
837
908
 
909
+ // Assembles one cell (window or forced-sticky) — the context, measure closure, and optional
910
+ // separator context — so both plan.cells and plan.forcedStickyCell build identically.
911
+ private buildWindowCell(
912
+ index: number,
913
+ key: string,
914
+ includeSeparator: boolean,
915
+ ): IWindowCell<ItemT> {
916
+ const item = this.getItem(this.data, index);
917
+ const context: IVListItemContext<ItemT> = {
918
+ $implicit: item,
919
+ index,
920
+ separators: this.makeSeparators(index),
921
+ };
922
+ return {
923
+ key,
924
+ index,
925
+ context,
926
+ measure: this.cellMeasure(index),
927
+ separatorContext: includeSeparator ? this.buildSeparatorContext(index, item) : undefined,
928
+ };
929
+ }
930
+
838
931
  private buildSeparatorContext(index: number, item: ItemT): IVListSeparatorContext<ItemT> {
839
932
  const overrides = this.separatorOverrides.get(index);
840
933
  const highlighted = overrides?.highlighted ?? false;
@@ -64,7 +64,7 @@ import {
64
64
  type IVListSeparatorContext,
65
65
  } from '../virtualized-list';
66
66
  import { VListOutletDirective } from '../virtualized-list/directives';
67
- import { stableAnchorStyle } from '../../primitives';
67
+ import { stableAnchorStyle, SymbioteStyleInputDirective } from '../../primitives';
68
68
  import {
69
69
  VSectionFooterDirective,
70
70
  VSectionHeaderDirective,
@@ -146,6 +146,7 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
146
146
  @Component({
147
147
  selector: 'VirtualizedSectionList',
148
148
  standalone: true,
149
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
149
150
  imports: [
150
151
  VirtualizedList,
151
152
  VListItemDirective,
package/src/components.ts CHANGED
@@ -134,6 +134,11 @@ export {
134
134
  RefreshControlHost,
135
135
  InputAccessoryViewHost,
136
136
  SymbioteHostPropsDirective,
137
+ // Public even though no app names it: it rides `hostDirectives` on every component declaring a
138
+ // `style` input, so ngtsc writes it into their public type metadata and a consuming app's AOT
139
+ // build fails with NG3004 without it. Invisible to tsc and vitest; only a consumer's ngc catches
140
+ // it.
141
+ SymbioteStyleInputDirective,
137
142
  // Exported so an OUT-OF-PACKAGE composed component (e.g. @symbiote-native/slider's Angular wrapper,
138
143
  // itself listed in ANCHOR_HOST_COMPONENTS) can merge its own anchor host's class-derived style
139
144
  // the same way every in-package composed component does — see anchorHostStyle's doc comment.
@@ -1,38 +1,32 @@
1
1
  // createPortal for @symbiote-native/angular — the Angular twin of the React/Vue same-surface portal
2
- // (create-portal.ts / Teleport in runtime-helpers.ts). Scope is identical: the target must be
3
- // an already-mounted location WITHIN THE SAME SURFACE as the portal's call site — moving content
4
- // across independently-mounted surfaces has no safe host-level primitive to hook into, so
5
- // createTunnel (create-tunnel.ts) covers that cross-surface case instead.
2
+ // (create-portal.ts / Teleport in runtime-helpers.ts). Scope is identical: the target must be an
3
+ // already-mounted location WITHIN THE SAME SURFACE as the portal's call site — moving content
4
+ // across independently-mounted surfaces has no safe host-level primitive, so createTunnel
5
+ // (create-tunnel.ts) covers that cross-surface case instead.
6
6
  //
7
- // React/Vue implement this as a thin validating wrapper around a mechanism the FRAMEWORK
8
- // ITSELF already has (react-reconciler's Fiber-level HostPortal, Vue's own <Teleport>) all
9
- // the actual node-moving is handled generically by the engine's insert/remove, which those
10
- // frameworks already call. Angular has no such built-in: there is no `@angular/cdk` dependency
11
- // here (core-only, see package.json), and physically moving an EmbeddedView's already-created
12
- // root nodes via Renderer2 after the fact is NOT safe Angular's own view-destroy path removes
13
- // a view's nodes from wherever ITS OWN bookkeeping thinks they live (the container's insertion
14
- // point), not from wherever a node was manually moved to afterwards, so a raw Renderer2 move
15
- // would desync Angular's internals from the retained tree.
7
+ // React/Vue implement this as a thin validating wrapper around a mechanism the framework itself
8
+ // already has (react-reconciler's Fiber-level HostPortal, Vue's own <Teleport>). Angular has no
9
+ // such built-in (no `@angular/cdk` dependency here, core-only see package.json), and physically
10
+ // moving an EmbeddedView's already-created root nodes via Renderer2 after the fact is NOT safe
11
+ // Angular's own view-destroy path removes a view's nodes from wherever ITS OWN bookkeeping thinks
12
+ // they live, not from wherever a node was manually moved to, so a raw Renderer2 move would desync
13
+ // Angular's internals from the retained tree.
16
14
  //
17
- // PortalDirective is a STRUCTURAL directive (`*portal="overlayHost"`), not a component that
18
- // takes a separate `<ng-template>` + `[content]` binding — that two-step reads as foreign to
19
- // anyone used to `*ngIf`/`*ngFor`/`*ngTemplateOutlet`, where the directive sits directly on the
20
- // content and its TemplateRef comes from injection, not a passed-in reference. `*portal="x"`
21
- // desugars the same way `*ngIf` does: Angular wraps the host element in an `<ng-template>` and
22
- // injects that template's own TemplateRef into the directive automatically.
15
+ // PortalDirective is a STRUCTURAL directive (`*portal="overlayHost"`), not a component taking a
16
+ // separate `<ng-template>` + `[content]` binding — that two-step reads as foreign next to
17
+ // `*ngIf`/`*ngFor`/`*ngTemplateOutlet`. `*portal="x"` desugars the same way `*ngIf` does: Angular
18
+ // wraps the host element in an `<ng-template>` and injects that template's own TemplateRef into
19
+ // the directive automatically.
23
20
  //
24
21
  // The safe, fully-public-API mechanism: create the embedded view DIRECTLY inside a
25
22
  // ViewContainerRef anchored at the destination, so there is nothing to move at all. That
26
- // ViewContainerRef has to come from somewhere in the destination's own template hence
27
- // `PortalOutletDirective`, a marker placed on the target host (`<View portalOutlet
28
- // #overlayHost="portalOutlet">`) purely to expose its ViewContainerRef, the same
29
- // export-as-template-variable idiom `#form="ngForm"` uses. This also replaces the
30
- // `isSymbioteNode` runtime guard React/Vue need: there `to` is an arbitrary JS value at
31
- // runtime (a wrong ref, a string, a plain object all type-check as `any`/`unknown` until
32
- // validated), but here `to` is typed as `PortalOutletDirective` the only way to construct
33
- // one is Angular's own template compiler resolving a template reference variable, so
34
- // `strictTemplates` rejects anything else at compile time and there is nothing left to guard
35
- // against at runtime.
23
+ // ViewContainerRef comes from `PortalOutletDirective`, a marker placed on the target host
24
+ // (`<View portalOutlet #overlayHost="portalOutlet">`) purely to expose its ViewContainerRef, the
25
+ // same export-as-template-variable idiom `#form="ngForm"` uses. This also replaces the
26
+ // `isSymbioteNode` runtime guard React/Vue need (there `to` is an arbitrary JS value until
27
+ // validated): here `to` is typed as `PortalOutletDirective` the only way to construct one is
28
+ // Angular's own template compiler resolving a template reference variable, so `strictTemplates`
29
+ // rejects anything else at compile time and there is nothing left to guard at runtime.
36
30
 
37
31
  import {
38
32
  Directive,
@@ -1,25 +1,22 @@
1
1
  // createTunnel for @symbiote-native/angular — the Angular twin of the React/Vue cross-surface tunnel
2
2
  // (adapters/react/src/create-tunnel.tsx, adapters/vue/src/create-tunnel.ts — read their
3
- // headers for the full "why not just extend the portal across surfaces" rationale; same prior
4
- // art: pmndrs/tunnel-rat, facebook/react#17147). `TunnelOut` lives in whichever component should
5
- // PAINT the content; its OWN read of the shared store drives that surface's own normal render
6
- // cycle — no cross-surface reach-in, no rootTag lookup, works whether TunnelIn and TunnelOut
7
- // share a surface or not.
3
+ // headers for the "why not just extend the portal across surfaces" rationale; same prior art:
4
+ // pmndrs/tunnel-rat, facebook/react#17147). `TunnelOut` lives in whichever component should PAINT
5
+ // the content; its own read of the shared store drives that surface's own normal render cycle —
6
+ // no cross-surface reach-in, works whether TunnelIn and TunnelOut share a surface or not.
8
7
  //
9
- // Angular cannot mirror React's/Vue's shape verbatim: both of those build a FRESH `In`/`Out`
10
- // pair per `createTunnel()` call (a closure-scoped class in Vue, closure-scoped functions in
11
- // React). Angular components can't be synthesized at runtime — there is no JIT compiler under
12
- // Metro/Hermes (see modules/animated/create-animated-component.ts's header for the same
8
+ // Angular cannot mirror React's/Vue's shape verbatim: both build a FRESH `In`/`Out` pair per
9
+ // `createTunnel()` call, and Angular components can't be synthesized at runtime — no JIT compiler
10
+ // under Metro/Hermes (see modules/animated/create-animated-component.ts's header for the same
13
11
  // constraint). So here `createTunnel()` returns only a plain reactive STORE (a signal);
14
12
  // `TunnelInDirective` and `TunnelOut` are ONE static, pre-authored, AOT-compilable pair,
15
- // parameterized by that store through an input — same relationship `VListOutletDirective` has
16
- // to the per-cell template it stamps, generalized to an open-ended, changing SET of templates.
13
+ // parameterized by that store through an input — the relationship `VListOutletDirective` has to
14
+ // the per-cell template it stamps, generalized to an open-ended, changing SET of templates.
17
15
  //
18
- // TunnelInDirective is a STRUCTURAL directive (`*tunnelIn="overlayTunnel"`), not a component
19
- // that takes a separate `<ng-template>` + `[content]` binding matching `*portal` (see
20
- // create-portal.ts's header for why that reads as native Angular and the two-step form
21
- // doesn't). `TunnelOut` stays a plain component (`<tunnel-out>`) — it's a rendering SLOT, the
22
- // same shape as Angular's own `<router-outlet>`, not content that needs a structural directive.
16
+ // TunnelInDirective is a STRUCTURAL directive (`*tunnelIn="overlayTunnel"`), matching `*portal`
17
+ // (see create-portal.ts's header for why that reads as native Angular). `TunnelOut` stays a plain
18
+ // component (`<tunnel-out>`) a rendering SLOT, the same shape as Angular's own
19
+ // `<router-outlet>`, not content that needs a structural directive.
23
20
  //
24
21
  // App code stays fully declarative — no ViewContainerRef/imperative rendering — by putting
25
22
  // `*tunnelIn` directly on the portable content, the same way `*ngIf` sits directly on an
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export {
27
27
  stableAnchorStyle,
28
28
  Switch,
29
29
  SymbioteHostPropsDirective,
30
+ SymbioteStyleInputDirective,
30
31
  Text,
31
32
  TextInput,
32
33
  TouchableHighlight,
@@ -249,7 +250,6 @@ export type {
249
250
  } from '@symbiote-native/engine';
250
251
  export {
251
252
  dlog,
252
- flattenStyle,
253
253
  isDebug,
254
254
  Platform,
255
255
  processColor,
@@ -259,3 +259,32 @@ export {
259
259
  StyleSheet,
260
260
  } from '@symbiote-native/engine';
261
261
  export type { IRootTag, ISymbioteEvent, ISymbioteNode } from '@symbiote-native/engine';
262
+ // The agnostic style / platform / view-config types behind the values above. Angular takes its
263
+ // component props as @Input()s and exports no per-component prop type, but an app still needs
264
+ // these to type a style object or a Platform.select spec.
265
+ export type {
266
+ IViewStyle,
267
+ ITextStyle,
268
+ IFlexAlign,
269
+ IFlexJustify,
270
+ IPlatformStatic,
271
+ IPlatformOSType,
272
+ IPlatformConstantsIOS,
273
+ IPlatformConstantsAndroid,
274
+ IPlatformSelectSpec,
275
+ INativeViewConfig,
276
+ INativeViewConfigSource,
277
+ } from '@symbiote-native/engine';
278
+ export type {
279
+ IAccessibilityProps,
280
+ IAriaProps,
281
+ IAccessibilityRole,
282
+ IRole,
283
+ IAccessibilityStateValue,
284
+ IAccessibilityValue,
285
+ IAccessibilityActionInfo,
286
+ IResponderProps,
287
+ IPressState,
288
+ IPressableAndroidRippleConfig,
289
+ IImageStatics,
290
+ } from '@symbiote-native/components';