@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/angular",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "SymbioteNative's Angular adapter — a Renderer2/RendererFactory2 with DOM-less bootstrap driving real native iOS/Android views through the same engine as the other adapters.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,24 +54,26 @@
54
54
  "access": "public"
55
55
  },
56
56
  "dependencies": {
57
- "@angular/compiler-cli": "^22",
57
+ "@angular/compiler-cli": "~22.0.8",
58
58
  "@react-native/typescript-config": "0.86.0",
59
- "@symbiote-native/components": "0.3.0",
60
- "@symbiote-native/css-parser": "0.2.3"
59
+ "@symbiote-native/components": "0.4.0",
60
+ "@symbiote-native/css-parser": "0.3.0"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "@angular/core": ">=20",
64
+ "@angular/forms": ">=20",
64
65
  "@symbiote-native/engine": ">=0.1.7",
65
66
  "react-native": ">=0.86"
66
67
  },
67
68
  "devDependencies": {
68
- "@angular/compiler": "^22",
69
- "@angular/core": "^22",
69
+ "@angular/compiler": "~22.0.8",
70
+ "@angular/core": "~22.0.8",
71
+ "@angular/forms": "~22.0.8",
70
72
  "@babel/core": "^7.25.2",
71
73
  "@types/babel__core": "^7.20.5",
72
74
  "@types/node": "^26.0.0",
73
75
  "typescript": "~6.0.0",
74
- "@symbiote-native/engine": "0.1.7",
76
+ "@symbiote-native/engine": "0.2.0",
75
77
  "@symbiote-native/test-utils": "0.1.6"
76
78
  },
77
79
  "scripts": {
@@ -11,11 +11,11 @@
11
11
  // Housing the registry here rather than inline in `renderer/index.ts` keeps it out of the require
12
12
  // cycle (cheap insurance against a load-order strand). The bug that actually surfaced this,
13
13
  // though, was STALE BUILD ARTIFACTS, not the cycle: `ngc -p` never deletes orphaned outputs, so
14
- // after the renderer moved `renderer.ts` `renderer/index.ts` the orphaned `build/angular/
14
+ // after the renderer moved `renderer.ts` -> `renderer/index.ts` the orphaned `build/angular/
15
15
  // renderer.js` lingered and SHADOWED `build/angular/renderer/index.js` (a file beats a folder in
16
16
  // Node/Metro resolution), giving the bundle a second, stale registry Set — fixed by cleaning
17
17
  // `build/` before every ngc build (see each Angular package's `clean` script). Full record:
18
- // angular-adapter §11c, symbiote-dev-examples §5e.
18
+ // angular-adapter §11c.
19
19
 
20
20
  // Lowercased at construction — isAnchorHostComponent and registerComposedComponent both normalize
21
21
  // to lowercase (Angular lowercases a dynamically-mounted component's selector at runtime, see
@@ -1,11 +1,13 @@
1
1
  import { ChangeDetectionStrategy, Component } from '@angular/core';
2
2
  import { DescriptorOutlet } from '../../descriptor-to-angular';
3
3
  import { ActivityIndicatorBase } from './shared';
4
+ import { SymbioteStyleInputDirective } from '../../primitives';
4
5
  export type { IActivityIndicatorProps } from './shared';
5
6
 
6
7
  @Component({
7
8
  selector: 'ActivityIndicator',
8
9
  standalone: true,
10
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
9
11
  imports: [DescriptorOutlet],
10
12
  changeDetection: ChangeDetectionStrategy.OnPush,
11
13
  template: `<symbiote-descriptor-outlet [node]="descriptor" />`,
@@ -1,6 +1,7 @@
1
1
  import { ChangeDetectionStrategy, Component } from '@angular/core';
2
2
  import { DescriptorOutlet } from '../../descriptor-to-angular';
3
3
  import { ActivityIndicatorBase } from './shared';
4
+ import { SymbioteStyleInputDirective } from '../../primitives';
4
5
  export type { IActivityIndicatorProps } from './shared';
5
6
 
6
7
  const IOS_DEFAULT_COLOR = '#999999';
@@ -8,6 +9,7 @@ const IOS_DEFAULT_COLOR = '#999999';
8
9
  @Component({
9
10
  selector: 'ActivityIndicator',
10
11
  standalone: true,
12
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
11
13
  imports: [DescriptorOutlet],
12
14
  changeDetection: ChangeDetectionStrategy.OnPush,
13
15
  template: `<symbiote-descriptor-outlet [node]="descriptor" />`,
@@ -1,19 +1,17 @@
1
1
  // Button for Angular. The minimal cross-platform button in RN's iOS shape (Button.js): a
2
2
  // TouchableOpacity wrapping a Text. The base text style, the role constant, and the color fold are
3
- // shared in @symbiote-native/components/view; here Angular only composes its TouchableOpacity + a
4
- // symbiote-text child and re-maps the few Button-owned props. The Angular twin of the React/Vue
5
- // adapter's Button. No JS-side platform branch, so this stays a flat single file.
3
+ // shared in @symbiote-native/components/view; Angular only composes its TouchableOpacity + a
4
+ // symbiote-text child and re-maps the few Button-owned props. No JS-side platform branch, so this
5
+ // stays a flat single file.
6
6
  //
7
- // RN's Button fixes accessibilityRole="button", marks the root accessible, and propagates `disabled`
8
- // into the accessibility state; those three win over any caller value. `title` becomes the Text
9
- // child; disabled forwards straight; touchSoundDisabled maps to the pressable's android_disableSound.
10
- // Every OTHER field of the shared IButtonProps (title, color, disabled, touchSoundDisabled, testID,
11
- // TV-focus, accessibility state) is agnostic and stays RE-EXPORTED from @symbiote-native/components
12
- // verbatim. onPress and the four accessibility callbacks are the
7
+ // RN's Button fixes accessibilityRole="button", marks the root accessible, and propagates
8
+ // `disabled` into the accessibility state; those three win over any caller value. `title` becomes
9
+ // the Text child; disabled forwards straight; touchSoundDisabled maps to the pressable's
10
+ // android_disableSound. Every other field of the shared IButtonProps stays RE-EXPORTED from
11
+ // @symbiote-native/components verbatim. onPress and the four accessibility callbacks are the
13
12
  // exceptions: each is a real @Output() here (`press`, `accessibilityAction`, `accessibilityTap`,
14
- // `magicTap`, `accessibilityEscape`), mirroring the Vue adapter's Button, which forks the SAME onPress
15
- // field the same way (`Omit<ICoreButtonProps, 'onPress'>` + a `press` emit) — not a new precedent, the
16
- // existing one extended to accessibility.
13
+ // `magicTap`, `accessibilityEscape`), mirroring the Vue adapter's Button, which forks the same
14
+ // onPress field the same way — not a new precedent, the existing one extended to accessibility.
17
15
 
18
16
  import {
19
17
  CUSTOM_ELEMENTS_SCHEMA,
@@ -24,6 +22,8 @@ import {
24
22
  inject,
25
23
  Input,
26
24
  Output,
25
+ signal,
26
+ type DoCheck,
27
27
  } from '@angular/core';
28
28
  import {
29
29
  BUTTON_ACCESSIBILITY_ROLE,
@@ -108,7 +108,7 @@ export type IButtonProps = Omit<
108
108
  </TouchableOpacity>
109
109
  `,
110
110
  })
111
- export class Button implements IButtonProps {
111
+ export class Button implements IButtonProps, DoCheck {
112
112
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
113
113
  // onto (see anchorHostStyle's doc comment in primitives/shared.ts) — NOT the inner
114
114
  // TouchableOpacity/Pressable one level down. RN's real Button takes no `style` prop, so this
@@ -187,7 +187,21 @@ export class Button implements IButtonProps {
187
187
  return resolveButtonTextStyle(this.color, this.disabled);
188
188
  }
189
189
 
190
+ // Polled, not read live: a class toggled after mount reaches the anchor through
191
+ // addClass/removeClass, which dirties nothing, so a plain getter froze at its creation value.
192
+ // Button cannot use SymbioteStyleInputDirective instead - RN's Button has no `style` prop, so
193
+ // there is no `style` @Input for it to hang off.
194
+ //
195
+ // ngDoCheck runs during the PARENT's refresh even when this view is skipped; the signal write is
196
+ // what marks this view. The anchor returns the same props.style reference until a commit changes
197
+ // it, so Object.is makes an unchanged poll free.
198
+ private readonly anchorStyleValue = signal<IStyleProp<IViewStyle> | undefined>(undefined);
199
+
200
+ ngDoCheck(): void {
201
+ this.anchorStyleValue.set(anchorStyleProp<IViewStyle>(this.elementRef));
202
+ }
203
+
190
204
  get anchorStyle(): IStyleProp<IViewStyle> | undefined {
191
- return anchorStyleProp<IViewStyle>(this.elementRef);
205
+ return this.anchorStyleValue();
192
206
  }
193
207
  }
@@ -1,37 +1,35 @@
1
1
  // FlatList, the Angular convenience surface over VirtualizedList. It takes a plain `data` array
2
2
  // and derives getItem/getItemCount; numColumns packs items into rows (each row a flex-row of N
3
- // columns), so the virtualized stream is rows, not items (RN's FlatList). All windowing /
4
- // viewability / batching / imperative scrolling are inherited from VirtualizedList; the data
5
- // shaping (chunkIntoRows / rowKeyExtractor / expandRowViewability / row-separator unwrap) is shared
6
- // verbatim from @symbiote-native/components, exactly as the React and Vue FlatLists reuse it. This is the
7
- // Angular twin of adapters/vue/src/components/flat-list/index.ts.
3
+ // columns), so the virtualized stream is rows, not items (RN's FlatList). Windowing / viewability /
4
+ // batching / imperative scrolling are inherited from VirtualizedList; the data shaping
5
+ // (chunkIntoRows / rowKeyExtractor / expandRowViewability / row-separator unwrap) is shared
6
+ // verbatim from @symbiote-native/components, same as React/Vue's FlatLists.
8
7
  //
9
- // TEMPLATE FORWARDING — both paths RE-STAMP; neither uses a bare `<ng-content>` passthrough.
8
+ // TEMPLATE FORWARDING — both column paths RE-STAMP; neither uses a bare `<ng-content>` passthrough.
10
9
  // Angular's @ContentChild does NOT resolve a directive across a SECOND `<ng-content>` re-projection
11
- // hop (it only sees what was projected directly onto the querying component's own tag) — a bare
12
- // `<VirtualizedList ...><ng-content></ng-content></VirtualizedList>` here left VirtualizedList's own
13
- // itemDir/headerDir/etc. undefined, so every cell rendered empty (a real, confirmed device bug see
14
- // flat-list.test.ts). Fixed for both column modes the same way:
10
+ // hop (only what's projected directly onto the querying component's own tag) — a bare
11
+ // `<VirtualizedList ...><ng-content></ng-content></VirtualizedList>` left VirtualizedList's own
12
+ // itemDir/headerDir/etc. undefined, rendering every cell empty (confirmed device bug, see
13
+ // flat-list.test.ts). Fixed for both column modes:
15
14
  // * Single column (numColumns <= 1): FlatList captures the app's `<ng-template vListItem>` (and
16
15
  // vListHeader/vListFooter/vListEmpty/vListSeparator) with its OWN @ContentChild — a single,
17
16
  // direct projection hop, which always resolves — then re-authors equivalent `<ng-template>`s on
18
- // `<VirtualizedList>`, each forwarding the captured templateRef + context through
17
+ // `<VirtualizedList>`, forwarding the captured templateRef + context through
19
18
  // VListOutletDirective. Item/index/separators pass through 1:1 (no row wrapping).
20
- // * Multi column (numColumns > 1): the same re-stamp, but the app's vListItem is typed for ItemT
21
- // while the virtualized stream is rows (IRow<ItemT>), so a plain passthrough couldn't work even if
22
- // the projection issue didn't exist. The row vListItem lays out the N columns side by side (each
23
- // cell stamps the app's item template via VListOutletDirective with the per-item context {item,
24
- // index: row.startIndex + column, separators}), the row vListSeparator unwraps the flanking rows
25
- // to their last/first item, and onViewableItemsChanged is wrapped with expandRowViewability so the
26
- // caller still sees per-item visibility. This mirrors exactly how Vue's FlatList intercepts
27
- // renderItem with renderRow.
19
+ // * Multi column (numColumns > 1): same re-stamp, but the app's vListItem is typed for ItemT
20
+ // while the virtualized stream is rows (IRow<ItemT>), so a plain passthrough couldn't work
21
+ // regardless. The row vListItem lays out the N columns side by side (each cell stamps the
22
+ // app's item template via VListOutletDirective with context {item, index: row.startIndex +
23
+ // column, separators}), the row vListSeparator unwraps the flanking rows to their last/first
24
+ // item, and onViewableItemsChanged is wrapped with expandRowViewability so the caller still
25
+ // sees per-item visibility mirrors how Vue's FlatList intercepts renderItem with renderRow.
28
26
  //
29
27
  // The imperative handle (scrollToIndex / scrollToOffset / scrollToEnd / scrollToItem /
30
28
  // recordInteraction / flashScrollIndicators / getScroll* / getNativeScrollRef) is RN's FlatList
31
- // surface; FlatList re-exposes it by delegating to the inner VirtualizedList (@ViewChild). The
32
- // element-returning props (renderItem / ItemSeparatorComponent / List{Header,Footer,Empty}Component)
33
- // are templates in Angular, so they are absent from IFlatListProps, per the per-adapter
34
- // children/render prop split; everything agnostic mirrors IVirtualizedListProps.
29
+ // surface, delegated straight to the inner VirtualizedList (@ViewChild). The element-returning
30
+ // props (renderItem / ItemSeparatorComponent / List{Header,Footer,Empty}Component) are templates
31
+ // in Angular, so they're absent from IFlatListProps per the per-adapter children/render split;
32
+ // everything else mirrors IVirtualizedListProps.
35
33
 
36
34
  import {
37
35
  CUSTOM_ELEMENTS_SCHEMA,
@@ -44,6 +42,7 @@ import {
44
42
  Output,
45
43
  ViewChild,
46
44
  inject,
45
+ type DoCheck,
47
46
  type OnChanges,
48
47
  type SimpleChanges,
49
48
  } from '@angular/core';
@@ -81,7 +80,7 @@ import {
81
80
  type IVListSeparatorContext,
82
81
  } from '../virtualized-list';
83
82
  import { VListOutletDirective } from '../virtualized-list/directives';
84
- import { stableAnchorStyle, ViewHost } from '../../primitives';
83
+ import { stableAnchorStyle, SymbioteStyleInputDirective, ViewHost } from '../../primitives';
85
84
 
86
85
  function isRecord(value: unknown): value is Record<string, unknown> {
87
86
  return typeof value === 'object' && value !== null;
@@ -165,6 +164,7 @@ export type IFlatListInputs<ItemT> = Omit<
165
164
  @Component({
166
165
  selector: 'FlatList',
167
166
  standalone: true,
167
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
168
168
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
169
169
  imports: [
170
170
  VirtualizedList,
@@ -338,7 +338,7 @@ export type IFlatListInputs<ItemT> = Omit<
338
338
  `,
339
339
  })
340
340
  export class FlatList<ItemT = unknown>
341
- implements IFlatListInputs<ItemT>, IVirtualizedListHandle, OnChanges
341
+ implements IFlatListInputs<ItemT>, IVirtualizedListHandle, DoCheck, OnChanges
342
342
  {
343
343
  // The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
344
344
  // `[onEndReached]="…"` — re-emitted straight from the inner VirtualizedList's own @Output()s (see
@@ -427,18 +427,16 @@ export class FlatList<ItemT = unknown>
427
427
  return this.columns > SINGLE_COLUMN;
428
428
  }
429
429
 
430
- ngOnChanges(changes: SimpleChanges): void {
431
- // Recomputed on EVERY ngOnChanges call, not gated on `changes['style']`: a class= on the
432
- // FlatList use site (no matching [style] binding at all) never appears in `changes`, so a
433
- // guard here would permanently skip picking up anchorHostStyle for that (the actual bug-report
434
- // shape) ngOnChanges still fires at least once at mount via the required `data` @Input(), and
435
- // by then the anchor's class-derived style is already resolved synchronously (routeProp runs
436
- // inline from Renderer2.addClass, before this component's own lifecycle hooks). stableAnchorStyle
437
- // (not a bare flattenStyle) keeps `resolvedStyle`'s REFERENCE stable across ticks where nothing
438
- // actually changed — `[style]="resolvedStyle"` binds onto VirtualizedList's own `@Input() style`,
439
- // which feeds its `ngDoCheck` dedup gate; a fresh object every tick would defeat that gate and
440
- // free-run change detection forever (see stableAnchorStyle's doc comment).
430
+ // ngDoCheck, NOT ngOnChanges: a bare `class=` never becomes an @Input, so ngOnChanges does not
431
+ // RUN for it and the merge froze at its creation value (a stranded tile on the ReactiveStyle
432
+ // canary). stableAnchorStyle, not a bare flattenStyle, keeps `resolvedStyle`'s reference stable
433
+ // across ticks that changed nothing - a fresh object every tick defeats VirtualizedList's own
434
+ // dedup gate and free-runs CD (see stableAnchorStyle's doc comment).
435
+ ngDoCheck(): void {
441
436
  this.resolvedStyle = stableAnchorStyle(this.elementRef, this.style, this.resolvedStyle);
437
+ }
438
+
439
+ ngOnChanges(changes: SimpleChanges): void {
442
440
  if (changes['columnWrapperStyle'] !== undefined) {
443
441
  // A class-name string resolves through the shared registry before flattenStyle, which
444
442
  // only understands style objects/arrays.
@@ -1,5 +1,18 @@
1
- import { CUSTOM_ELEMENTS_SCHEMA, Component, ElementRef, inject } from '@angular/core';
2
- import { anchorHostStyle, ImageHost, SymbioteHostPropsDirective } from '../../primitives';
1
+ import {
2
+ CUSTOM_ELEMENTS_SCHEMA,
3
+ Component,
4
+ ElementRef,
5
+ inject,
6
+ signal,
7
+ type DoCheck,
8
+ type OnChanges,
9
+ } from '@angular/core';
10
+ import {
11
+ anchorHostStyle,
12
+ ImageHost,
13
+ SymbioteHostPropsDirective,
14
+ SymbioteStyleInputDirective,
15
+ } from '../../primitives';
3
16
  import { IMAGE_INPUTS, IMAGE_OUTPUTS, ImageBase, resolveImageProps } from './shared';
4
17
  export { setImageSourceResolver } from './shared';
5
18
  export type {
@@ -14,13 +27,14 @@ export type {
14
27
  @Component({
15
28
  selector: 'Image',
16
29
  standalone: true,
30
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
17
31
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
18
32
  imports: [ImageHost, SymbioteHostPropsDirective],
19
33
  inputs: IMAGE_INPUTS,
20
34
  outputs: IMAGE_OUTPUTS,
21
35
  template: `
22
36
  <symbiote-image
23
- [symbioteHostProps]="imageProps"
37
+ [symbioteHostProps]="imageProps()"
24
38
  (accessibilityAction)="handleAccessibilityAction($event)"
25
39
  (accessibilityTap)="handleAccessibilityTap($event)"
26
40
  (magicTap)="handleMagicTap($event)"
@@ -34,15 +48,45 @@ export type {
34
48
  />
35
49
  `,
36
50
  })
37
- export class Image extends ImageBase {
51
+ export class Image extends ImageBase implements OnChanges, DoCheck {
38
52
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
39
53
  // onto (see anchorHostStyle's doc comment) — NOT the inner `symbiote-image` one level down.
40
54
  private readonly elementRef = inject(ElementRef);
41
55
 
42
- override get imageProps(): Record<string, unknown> {
56
+ // The anchor's class-derived style is written by the renderer's addClass/removeClass and never
57
+ // reaches Angular's input pipeline (a bare `class="x"` never appears in SimpleChanges — see
58
+ // anchorHostStyle's doc comment), so ImageBase's inputsRevision cannot see it. Hence its own
59
+ // signal: a plain field read inside a computed() would be untracked and the bag would go stale
60
+ // the moment a `[class.x]`/`[ngClass]` toggled.
61
+ //
62
+ // ngDoCheck is the right cadence: render3's callHooks flushes a node's pre-order hooks at the
63
+ // next `ɵɵadvance` past it (or in refreshView's post-template flush), i.e. AFTER the declaring
64
+ // template's ɵɵclassProp wrote the class onto this element and BEFORE this component's own view
65
+ // refreshes — the exact window a class poll needs. callHooks also runs under
66
+ // setActiveConsumer(null), so the write registers no dependency on the caller's view (no NG0600).
67
+ // commitClassStyle allocates a fresh style array only when a token actually moved, so signal.set's
68
+ // own Object.is check makes an unchanged poll a no-op that dirties nothing.
69
+ //
70
+ // Known gap, shared with every other component polling this way (Pressable, SafeAreaView,
71
+ // TouchableNativeFeedback): `[ngClass]` applies its tokens from NgClass's OWN ngDoCheck, and a
72
+ // node's hooks flush in registration order with the component's first, so an [ngClass] flip lands
73
+ // one pass after this poll reads it. `class=` and `[class.x]` are written by the declaring
74
+ // template's styling instructions and so are always current here.
75
+ private readonly anchorStyle = signal<unknown>(undefined);
76
+
77
+ // ImageBase is undecorated, so the input bump is declared here rather than inherited.
78
+ ngOnChanges(): void {
79
+ this.bumpInputsRevision();
80
+ }
81
+
82
+ ngDoCheck(): void {
83
+ this.anchorStyle.set(anchorHostStyle(this.elementRef));
84
+ }
85
+
86
+ protected override buildImageProps(): Record<string, unknown> {
43
87
  return resolveImageProps({
44
88
  ...this.imageInputProps,
45
- style: [anchorHostStyle(this.elementRef), this.style],
89
+ style: [this.anchorStyle(), this.style],
46
90
  });
47
91
  }
48
92
  }
@@ -1,5 +1,18 @@
1
- import { CUSTOM_ELEMENTS_SCHEMA, Component, ElementRef, inject } from '@angular/core';
2
- import { anchorHostStyle, ImageHost, SymbioteHostPropsDirective } from '../../primitives';
1
+ import {
2
+ CUSTOM_ELEMENTS_SCHEMA,
3
+ Component,
4
+ ElementRef,
5
+ inject,
6
+ signal,
7
+ type DoCheck,
8
+ type OnChanges,
9
+ } from '@angular/core';
10
+ import {
11
+ anchorHostStyle,
12
+ ImageHost,
13
+ SymbioteHostPropsDirective,
14
+ SymbioteStyleInputDirective,
15
+ } from '../../primitives';
3
16
  import { IMAGE_INPUTS, IMAGE_OUTPUTS, ImageBase, resolveImageProps } from './shared';
4
17
  export { setImageSourceResolver } from './shared';
5
18
  export type {
@@ -14,13 +27,14 @@ export type {
14
27
  @Component({
15
28
  selector: 'Image',
16
29
  standalone: true,
30
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
17
31
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
18
32
  imports: [ImageHost, SymbioteHostPropsDirective],
19
33
  inputs: IMAGE_INPUTS,
20
34
  outputs: IMAGE_OUTPUTS,
21
35
  template: `
22
36
  <symbiote-image
23
- [symbioteHostProps]="imageProps"
37
+ [symbioteHostProps]="imageProps()"
24
38
  (accessibilityAction)="handleAccessibilityAction($event)"
25
39
  (accessibilityTap)="handleAccessibilityTap($event)"
26
40
  (magicTap)="handleMagicTap($event)"
@@ -34,15 +48,45 @@ export type {
34
48
  />
35
49
  `,
36
50
  })
37
- export class Image extends ImageBase {
51
+ export class Image extends ImageBase implements OnChanges, DoCheck {
38
52
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
39
53
  // onto (see anchorHostStyle's doc comment) — NOT the inner `symbiote-image` one level down.
40
54
  private readonly elementRef = inject(ElementRef);
41
55
 
42
- override get imageProps(): Record<string, unknown> {
56
+ // The anchor's class-derived style is written by the renderer's addClass/removeClass and never
57
+ // reaches Angular's input pipeline (a bare `class="x"` never appears in SimpleChanges — see
58
+ // anchorHostStyle's doc comment), so ImageBase's inputsRevision cannot see it. Hence its own
59
+ // signal: a plain field read inside a computed() would be untracked and the bag would go stale
60
+ // the moment a `[class.x]`/`[ngClass]` toggled.
61
+ //
62
+ // ngDoCheck is the right cadence: render3's callHooks flushes a node's pre-order hooks at the
63
+ // next `ɵɵadvance` past it (or in refreshView's post-template flush), i.e. AFTER the declaring
64
+ // template's ɵɵclassProp wrote the class onto this element and BEFORE this component's own view
65
+ // refreshes — the exact window a class poll needs. callHooks also runs under
66
+ // setActiveConsumer(null), so the write registers no dependency on the caller's view (no NG0600).
67
+ // commitClassStyle allocates a fresh style array only when a token actually moved, so signal.set's
68
+ // own Object.is check makes an unchanged poll a no-op that dirties nothing.
69
+ //
70
+ // Known gap, shared with every other component polling this way (Pressable, SafeAreaView,
71
+ // TouchableNativeFeedback): `[ngClass]` applies its tokens from NgClass's OWN ngDoCheck, and a
72
+ // node's hooks flush in registration order with the component's first, so an [ngClass] flip lands
73
+ // one pass after this poll reads it. `class=` and `[class.x]` are written by the declaring
74
+ // template's styling instructions and so are always current here.
75
+ private readonly anchorStyle = signal<unknown>(undefined);
76
+
77
+ // ImageBase is undecorated, so the input bump is declared here rather than inherited.
78
+ ngOnChanges(): void {
79
+ this.bumpInputsRevision();
80
+ }
81
+
82
+ ngDoCheck(): void {
83
+ this.anchorStyle.set(anchorHostStyle(this.elementRef));
84
+ }
85
+
86
+ protected override buildImageProps(): Record<string, unknown> {
43
87
  return resolveImageProps({
44
88
  ...this.imageInputProps,
45
- style: [anchorHostStyle(this.elementRef), this.style],
89
+ style: [this.anchorStyle(), this.style],
46
90
  });
47
91
  }
48
92
  }
@@ -1,4 +1,4 @@
1
- import { EventEmitter } from '@angular/core';
1
+ import { EventEmitter, computed, signal } from '@angular/core';
2
2
  import {
3
3
  imageStatics,
4
4
  renderImage,
@@ -473,7 +473,39 @@ export abstract class ImageBase {
473
473
  };
474
474
  }
475
475
 
476
- get imageProps(): Record<string, unknown> {
476
+ // Bridges the non-reactive @Input fields into the reactive graph so imageProps below can be a
477
+ // memoized computed(). Signal inputs would make this unnecessary, but `input()` is visible only
478
+ // to the AOT compiler and this package's unit suite runs on JIT - see the
479
+ // `angular-adapter-change-detection` skill, §6. `signal()`/`computed()` are plain runtime APIs
480
+ // and need no compiler support, which is what makes this bridge possible at all.
481
+ //
482
+ // ImageBase is deliberately UNdecorated, so the ngOnChanges that bumps this lives on each
483
+ // platform @Component instead of here.
484
+ //
485
+ // Every dependency of the bag below is an @Input, which Angular always routes through
486
+ // ngOnChanges. State assigned OUTSIDE that path needs its OWN signal - never widen this one to
487
+ // "bump on everything", which silently un-memoizes the bag. The platform anchor style is exactly
488
+ // such a dependency and carries its own signal.
489
+ protected readonly inputsRevision = signal(0);
490
+
491
+ protected bumpInputsRevision(): void {
492
+ this.inputsRevision.update(revision => revision + 1);
493
+ }
494
+
495
+ // MEASURED 2026-08-16: as a getter this rebuilt the bag on every refresh of this view, so
496
+ // `[symbioteHostProps]`'s reference check failed every time and every key was re-pushed through
497
+ // Renderer2 -> routeProp - pure waste when nothing changed. computed() returns the SAME object
498
+ // until a tracked dependency actually changes, and the input setter then skips the whole spread.
499
+ //
500
+ // A computed FIELD cannot be overridden by a subclass accessor the way a getter can, so the
501
+ // platform components override buildImageProps() below and this stays the single memoization
502
+ // point for every platform.
503
+ readonly imageProps = computed<Record<string, unknown>>(() => {
504
+ this.inputsRevision();
505
+ return this.buildImageProps();
506
+ });
507
+
508
+ protected buildImageProps(): Record<string, unknown> {
477
509
  return resolveImageProps(this.imageInputProps);
478
510
  }
479
511
  }
@@ -37,7 +37,7 @@ import {
37
37
  type IViewStyle,
38
38
  } from '@symbiote-native/engine';
39
39
  import { Image } from './image';
40
- import { anchorHostStyle, ViewHost } from '../primitives';
40
+ import { anchorHostStyle, SymbioteStyleInputDirective, ViewHost } from '../primitives';
41
41
 
42
42
  // Mirrors React's IImageBackgroundProps minus children (Angular takes children via <ng-content>):
43
43
  // every forwarding Image prop flows onto the inner image; `style` is the WRAPPER View style and
@@ -68,6 +68,7 @@ export type IAngularImageBackgroundInputs = Omit<
68
68
  @Component({
69
69
  selector: 'ImageBackground',
70
70
  standalone: true,
71
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
71
72
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
72
73
  imports: [Image, ViewHost],
73
74
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -11,11 +11,15 @@ import {
11
11
  CUSTOM_ELEMENTS_SCHEMA,
12
12
  ChangeDetectionStrategy,
13
13
  Component,
14
+ computed,
14
15
  ElementRef,
15
16
  EventEmitter,
16
17
  inject,
17
18
  Input,
18
19
  Output,
20
+ signal,
21
+ type DoCheck,
22
+ type OnChanges,
19
23
  } from '@angular/core';
20
24
  import {
21
25
  renderInputAccessoryView,
@@ -34,6 +38,7 @@ import {
34
38
  anchorHostStyle,
35
39
  InputAccessoryViewHost,
36
40
  SymbioteHostPropsDirective,
41
+ SymbioteStyleInputDirective,
37
42
  } from '../../primitives';
38
43
 
39
44
  // Mirrors React's IInputAccessoryViewProps minus children (Angular takes children via
@@ -56,12 +61,13 @@ export type IAngularInputAccessoryViewInputs = Omit<
56
61
  @Component({
57
62
  selector: 'InputAccessoryView',
58
63
  standalone: true,
64
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
59
65
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
60
66
  imports: [InputAccessoryViewHost, SymbioteHostPropsDirective],
61
67
  changeDetection: ChangeDetectionStrategy.OnPush,
62
68
  template: `
63
69
  <symbiote-input-accessory-view
64
- [symbioteHostProps]="hostProps"
70
+ [symbioteHostProps]="hostProps()"
65
71
  (accessibilityAction)="emit(accessibilityAction, $event)"
66
72
  (accessibilityTap)="emit(accessibilityTap, $event)"
67
73
  (magicTap)="emit(magicTap, $event)"
@@ -71,7 +77,7 @@ export type IAngularInputAccessoryViewInputs = Omit<
71
77
  </symbiote-input-accessory-view>
72
78
  `,
73
79
  })
74
- export class InputAccessoryView implements IAngularInputAccessoryViewInputs {
80
+ export class InputAccessoryView implements IAngularInputAccessoryViewInputs, OnChanges, DoCheck {
75
81
  @Input() nativeID?: string;
76
82
  @Input() backgroundColor?: string;
77
83
  @Input() style?: IStyleProp<IViewStyle>;
@@ -135,12 +141,44 @@ export class InputAccessoryView implements IAngularInputAccessoryViewInputs {
135
141
  });
136
142
  }
137
143
 
144
+ // Bridges the non-reactive fields `hostProps` reads into the reactive graph, so it can memoize.
145
+ // Plain fields read inside a computed() are UNTRACKED - something must signal "a dependency
146
+ // changed" or the bag goes stale. Signal inputs would do this natively, but `input()` is visible
147
+ // only to the AOT compiler and this package's unit suite runs on JIT (see the
148
+ // `angular-adapter-change-detection` skill, §6); `signal()`/`computed()` are plain runtime APIs.
149
+ private readonly hostPropsRevision = signal(0);
150
+ // What the anchor's class-derived style was when the bag was last built (identity, not value).
151
+ private lastAnchorStyle: unknown;
152
+
153
+ ngOnChanges(): void {
154
+ // The single moment Angular has finished writing every changed @Input for this pass.
155
+ this.hostPropsRevision.update(revision => revision + 1);
156
+ }
157
+
158
+ // The anchor's class-derived style is NOT an @Input: `class="..."`/`[ngClass]` at the use site
159
+ // resolves through the renderer's addClass onto this component's anchor host (see
160
+ // anchorHostStyle's doc comment), so it never shows up in SimpleChanges and ngOnChanges alone
161
+ // would leave a later class toggle stranded. ngDoCheck runs at exactly the cadence the old
162
+ // getter was re-read, and bumps only on a real identity change.
163
+ ngDoCheck(): void {
164
+ const anchorStyle = anchorHostStyle(this.elementRef);
165
+ if (anchorStyle === this.lastAnchorStyle) return;
166
+ this.lastAnchorStyle = anchorStyle;
167
+ this.hostPropsRevision.update(revision => revision + 1);
168
+ }
169
+
138
170
  // The anchor's class-derived style goes FIRST, the Descriptor's resolved style SECOND —
139
171
  // flattenStyle's later-wins collapse keeps an explicit [style] winning over its ambient class.
140
- get hostProps(): Record<string, unknown> {
172
+ //
173
+ // A computed(), not a getter: Angular re-reads a template getter on every refresh of this view
174
+ // and `[symbioteHostProps]` compares by REFERENCE, so a freshly rebuilt (but identical) bag
175
+ // re-pushes every key through renderer.setProperty -> the engine's prop routing - and this one
176
+ // re-runs renderInputAccessoryView to build it.
177
+ readonly hostProps = computed<Record<string, unknown>>(() => {
178
+ this.hostPropsRevision();
141
179
  const descriptorProps = this.descriptor.props;
142
180
  return { ...descriptorProps, style: [anchorHostStyle(this.elementRef), descriptorProps.style] };
143
- }
181
+ });
144
182
 
145
183
  // Typed as the a11y intersection WITH the string index (the bag renderInputAccessoryView spreads
146
184
  // into the host props), so resolveAccessibilityProps's result stays assignable to passthrough.
@@ -48,7 +48,12 @@ import {
48
48
  type ISymbioteEvent,
49
49
  type IViewStyle,
50
50
  } from '@symbiote-native/engine';
51
- import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '../../primitives';
51
+ import {
52
+ anchorHostStyle,
53
+ SymbioteHostPropsDirective,
54
+ SymbioteStyleInputDirective,
55
+ ViewHost,
56
+ } from '../../primitives';
52
57
 
53
58
  export type { IKeyboardAvoidingBehavior } from '@symbiote-native/components';
54
59
 
@@ -79,6 +84,7 @@ export type IAngularKeyboardAvoidingViewInputs = Omit<
79
84
  @Component({
80
85
  selector: 'KeyboardAvoidingView',
81
86
  standalone: true,
87
+ hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
82
88
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
83
89
  imports: [ViewHost, SymbioteHostPropsDirective],
84
90
  changeDetection: ChangeDetectionStrategy.OnPush,