@symbiote-native/angular 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +3 -3
  2. package/babel-lower-host-primitives.cjs +636 -0
  3. package/babel-register-composed.cjs +21 -3
  4. package/build/angular/anchor-host-registry.js +3 -1
  5. package/build/angular/anchor-host-registry.js.map +1 -1
  6. package/build/angular/bootstrap.js +1 -1
  7. package/build/angular/bootstrap.js.map +1 -1
  8. package/build/angular/components/activity-indicator/index.android.js +7 -2
  9. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  10. package/build/angular/components/activity-indicator/index.ios.js +3 -1
  11. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  12. package/build/angular/components/activity-indicator/shared.d.ts +1 -1
  13. package/build/angular/components/activity-indicator/shared.js +1 -1
  14. package/build/angular/components/activity-indicator/shared.js.map +1 -1
  15. package/build/angular/components/button.d.ts +3 -0
  16. package/build/angular/components/button.js +22 -5
  17. package/build/angular/components/button.js.map +1 -1
  18. package/build/angular/components/flat-list/index.d.ts +1 -1
  19. package/build/angular/components/flat-list/index.js +47 -14
  20. package/build/angular/components/flat-list/index.js.map +1 -1
  21. package/build/angular/components/image/index.android.js +4 -10
  22. package/build/angular/components/image/index.android.js.map +1 -1
  23. package/build/angular/components/image/index.ios.js +4 -10
  24. package/build/angular/components/image/index.ios.js.map +1 -1
  25. package/build/angular/components/image/shared.d.ts +4 -4
  26. package/build/angular/components/image/shared.js +51 -33
  27. package/build/angular/components/image/shared.js.map +1 -1
  28. package/build/angular/components/image-background.d.ts +3 -0
  29. package/build/angular/components/image-background.js +32 -9
  30. package/build/angular/components/image-background.js.map +1 -1
  31. package/build/angular/components/input-accessory-view/index.d.ts +2 -1
  32. package/build/angular/components/input-accessory-view/index.js +18 -16
  33. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  34. package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
  35. package/build/angular/components/keyboard-avoiding-view/index.js +56 -19
  36. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  37. package/build/angular/components/modal/index.d.ts +1 -0
  38. package/build/angular/components/modal/index.js +21 -10
  39. package/build/angular/components/modal/index.js.map +1 -1
  40. package/build/angular/components/pressable/index.d.ts +10 -5
  41. package/build/angular/components/pressable/index.js +61 -16
  42. package/build/angular/components/pressable/index.js.map +1 -1
  43. package/build/angular/components/refresh-control/index.d.ts +2 -1
  44. package/build/angular/components/refresh-control/index.js +13 -9
  45. package/build/angular/components/refresh-control/index.js.map +1 -1
  46. package/build/angular/components/safe-area-view/index.d.ts +3 -1
  47. package/build/angular/components/safe-area-view/index.js +22 -20
  48. package/build/angular/components/safe-area-view/index.js.map +1 -1
  49. package/build/angular/components/scroll-view/index.android.js +44 -14
  50. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  51. package/build/angular/components/scroll-view/index.ios.js +13 -5
  52. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  53. package/build/angular/components/scroll-view/projection.d.ts +32 -0
  54. package/build/angular/components/scroll-view/projection.js +212 -47
  55. package/build/angular/components/scroll-view/projection.js.map +1 -1
  56. package/build/angular/components/scroll-view/shared.d.ts +2 -0
  57. package/build/angular/components/scroll-view/shared.js +48 -11
  58. package/build/angular/components/scroll-view/shared.js.map +1 -1
  59. package/build/angular/components/scroll-view/sticky-header.js +7 -3
  60. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  61. package/build/angular/components/section-list/index.d.ts +9 -1
  62. package/build/angular/components/section-list/index.js +72 -18
  63. package/build/angular/components/section-list/index.js.map +1 -1
  64. package/build/angular/components/switch/index.android.js +23 -9
  65. package/build/angular/components/switch/index.android.js.map +1 -1
  66. package/build/angular/components/switch/index.ios.js +23 -9
  67. package/build/angular/components/switch/index.ios.js.map +1 -1
  68. package/build/angular/components/switch/shared.d.ts +1 -1
  69. package/build/angular/components/switch/shared.js +5 -2
  70. package/build/angular/components/switch/shared.js.map +1 -1
  71. package/build/angular/components/text-input.d.ts +8 -2
  72. package/build/angular/components/text-input.js +79 -34
  73. package/build/angular/components/text-input.js.map +1 -1
  74. package/build/angular/components/touchable/index.d.ts +36 -7
  75. package/build/angular/components/touchable/index.js +280 -72
  76. package/build/angular/components/touchable/index.js.map +1 -1
  77. package/build/angular/components/touchable-native-feedback/index.d.ts +2 -1
  78. package/build/angular/components/touchable-native-feedback/index.js +19 -17
  79. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  80. package/build/angular/components/virtualized-list/directives.js +4 -0
  81. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  82. package/build/angular/components/virtualized-list/index.d.ts +8 -2
  83. package/build/angular/components/virtualized-list/index.js +207 -61
  84. package/build/angular/components/virtualized-list/index.js.map +1 -1
  85. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  86. package/build/angular/components/virtualized-section-list/index.d.ts +21 -1
  87. package/build/angular/components/virtualized-section-list/index.js +100 -22
  88. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  89. package/build/angular/components.d.ts +2 -2
  90. package/build/angular/components.js +1 -1
  91. package/build/angular/components.js.map +1 -1
  92. package/build/angular/create-portal/index.d.ts +11 -2
  93. package/build/angular/create-portal/index.js +42 -12
  94. package/build/angular/create-portal/index.js.map +1 -1
  95. package/build/angular/create-tunnel/index.js.map +1 -1
  96. package/build/angular/descriptor-to-angular/index.js +3 -1
  97. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  98. package/build/angular/diagnostics.d.ts +33 -0
  99. package/build/angular/diagnostics.js +82 -0
  100. package/build/angular/diagnostics.js.map +1 -0
  101. package/build/angular/gate-demand.d.ts +37 -0
  102. package/build/angular/gate-demand.js +84 -0
  103. package/build/angular/gate-demand.js.map +1 -0
  104. package/build/angular/host-instance/index.js +1 -1
  105. package/build/angular/host-instance/index.js.map +1 -1
  106. package/build/angular/index.d.ts +6 -2
  107. package/build/angular/index.js +16 -1
  108. package/build/angular/index.js.map +1 -1
  109. package/build/angular/modules/animated/create-animated-component.d.ts +1 -0
  110. package/build/angular/modules/animated/create-animated-component.js +41 -18
  111. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  112. package/build/angular/modules/app-registry/index.js.map +1 -1
  113. package/build/angular/modules/status-bar/index.js +1 -1
  114. package/build/angular/modules/status-bar/index.js.map +1 -1
  115. package/build/angular/primitives/index.d.ts +31 -2
  116. package/build/angular/primitives/index.js +81 -3
  117. package/build/angular/primitives/index.js.map +1 -1
  118. package/build/angular/primitives/shared.d.ts +9 -1
  119. package/build/angular/primitives/shared.js +49 -3
  120. package/build/angular/primitives/shared.js.map +1 -1
  121. package/build/angular/register.d.ts +1 -0
  122. package/build/angular/register.js +35 -0
  123. package/build/angular/register.js.map +1 -0
  124. package/build/angular/render/index.js +51 -6
  125. package/build/angular/render/index.js.map +1 -1
  126. package/build/angular/renderer/index.d.ts +1 -0
  127. package/build/angular/renderer/index.js +157 -21
  128. package/build/angular/renderer/index.js.map +1 -1
  129. package/build/angular/services/color-scheme.service.js.map +1 -1
  130. package/build/angular/services/window-dimensions.service.js +1 -1
  131. package/build/angular/services/window-dimensions.service.js.map +1 -1
  132. package/build/angular/state-style.d.ts +9 -0
  133. package/build/angular/state-style.js +39 -0
  134. package/build/angular/state-style.js.map +1 -0
  135. package/metro-transformer.cjs +69 -0
  136. package/package.json +27 -8
  137. package/src/anchor-host-registry.ts +3 -1
  138. package/src/bootstrap.ts +4 -1
  139. package/src/components/activity-indicator/index.android.ts +7 -2
  140. package/src/components/activity-indicator/index.ios.ts +3 -1
  141. package/src/components/activity-indicator/shared.ts +32 -13
  142. package/src/components/button.ts +36 -6
  143. package/src/components/flat-list/index.ts +91 -29
  144. package/src/components/image/index.android.ts +9 -6
  145. package/src/components/image/index.ios.ts +9 -6
  146. package/src/components/image/shared.ts +110 -45
  147. package/src/components/image-background.ts +47 -10
  148. package/src/components/input-accessory-view/index.ts +41 -18
  149. package/src/components/keyboard-avoiding-view/index.ts +76 -22
  150. package/src/components/modal/index.ts +39 -12
  151. package/src/components/pressable/index.ts +121 -31
  152. package/src/components/refresh-control/index.ts +36 -13
  153. package/src/components/safe-area-view/index.ts +44 -19
  154. package/src/components/scroll-view/index.android.ts +31 -9
  155. package/src/components/scroll-view/index.ios.ts +17 -4
  156. package/src/components/scroll-view/projection.ts +290 -63
  157. package/src/components/scroll-view/shared.ts +124 -30
  158. package/src/components/scroll-view/sticky-header.ts +25 -8
  159. package/src/components/section-list/index.ts +79 -19
  160. package/src/components/switch/index.android.ts +18 -7
  161. package/src/components/switch/index.ios.ts +18 -7
  162. package/src/components/switch/shared.ts +15 -4
  163. package/src/components/text-input.ts +119 -31
  164. package/src/components/touchable/index.ts +339 -72
  165. package/src/components/touchable-native-feedback/index.ts +50 -18
  166. package/src/components/virtualized-list/directives.ts +19 -5
  167. package/src/components/virtualized-list/index.ts +292 -72
  168. package/src/components/virtualized-section-list/directives.ts +6 -3
  169. package/src/components/virtualized-section-list/index.ts +164 -34
  170. package/src/components.ts +6 -1
  171. package/src/create-portal/index.ts +47 -11
  172. package/src/create-tunnel/index.ts +3 -1
  173. package/src/descriptor-to-angular/index.ts +29 -8
  174. package/src/diagnostics.ts +138 -0
  175. package/src/gate-demand.ts +109 -0
  176. package/src/host-instance/index.ts +9 -3
  177. package/src/index.ts +33 -2
  178. package/src/modules/animated/create-animated-component.ts +67 -19
  179. package/src/modules/app-registry/index.ts +3 -1
  180. package/src/modules/status-bar/index.ts +13 -4
  181. package/src/primitives/index.ts +73 -2
  182. package/src/primitives/shared.ts +70 -9
  183. package/src/register.ts +43 -0
  184. package/src/render/index.ts +59 -7
  185. package/src/renderer/index.ts +209 -37
  186. package/src/services/color-scheme.service.ts +3 -1
  187. package/src/services/window-dimensions.service.ts +11 -4
  188. package/src/state-style.ts +39 -0
@@ -0,0 +1,138 @@
1
+ // Per-window counters for the Angular adapter, the twin of the engine's readCommitProfile().
2
+ // The engine's number answers "what did the commit walk cost"; these answer the question one layer
3
+ // up, which is the one Angular keeps losing: HOW MANY TIMES did anything run at all.
4
+ //
5
+ // Angular is the only adapter whose update model can free-run. A change-detection pass that writes
6
+ // a fresh reference into a downstream @Input marks a view dirty, the zoneless scheduler books
7
+ // another pass, and it never settles - the hazard `stableAnchorStyle` and VirtualizedList's
8
+ // `lastRecompute` gate each guard one instance of. A guard that stops holding is INVISIBLE from the
9
+ // outside: the screen paints correctly, only the frame rate collapses. `cdPasses` at rest is what
10
+ // makes it visible, and every other counter here narrows the same window to a suspect.
11
+ //
12
+ // Deliberately NOT gated behind isDebug(): an integer increment is noise next to the work it
13
+ // counts, and the numbers are only meaningful from a RELEASE build (dev-mode Angular runs its own
14
+ // checks and drowns the signal). The per-name detail map IS gated, since it allocates.
15
+
16
+ export interface IAngularProfile {
17
+ // Change-detection passes: RendererFactory2.end() runs once per ApplicationRef.tick().
18
+ cdPasses: number;
19
+ // Primitive host views (symbiote-view / symbiote-text / ...) Angular actually walked. Divided by
20
+ // cdPasses this is the SIZE OF THE TREE one tick touches - the number that separates "the list is
21
+ // expensive" from "every mounted screen is re-checked because a list scrolled". The primitives are
22
+ // CheckAlways, and a Global tick refreshes every CheckAlways view in the application.
23
+ viewsChecked: number;
24
+ // Prop/style/class/text writes that reached the engine. At rest this must be 0: a nonzero idle
25
+ // value means change detection is rewriting props nothing asked to change.
26
+ rendererWrites: number;
27
+ nodesCreated: number;
28
+ nodesInserted: number;
29
+ nodesRemoved: number;
30
+ // The `class` poll every COMPOSED component runs per check (SymbioteStyleInputDirective: Pressable,
31
+ // ScrollView, Image, the lists...; the raw primitives are counted by `viewsChecked` instead).
32
+ // `styleMarks` is how often the poll actually found a change and dirtied the view - the ratio is
33
+ // how much of the polling is wasted, and a nonzero idle `styleMarks` is a free-run engine.
34
+ styleChecks: number;
35
+ styleMarks: number;
36
+ // VirtualizedList: checks vs the ones that got past the `lastRecompute` dedup gate. Recomputes
37
+ // climbing with checks means the gate is not holding, which is the free-run bug itself.
38
+ listChecks: number;
39
+ listRecomputes: number;
40
+ listMarks: number;
41
+ // Scroll events the list saw. With sticky headers RN pins scrollEventThrottle to 1, so this is
42
+ // the frame rate of the gesture and the denominator for everything measured during a drag.
43
+ scrollTicks: number;
44
+ // Cell views. `outletUpdates` is the cheap path (context patched in place); `outletCreates` past
45
+ // the first window means cells are being rebuilt instead of reused, which is the cost CDK's
46
+ // view-recycling strategy exists to avoid.
47
+ outletCreates: number;
48
+ outletUpdates: number;
49
+ outletDestroys: number;
50
+ // Sticky projection: schedules are cheap (a Set insert), flushes walk every projected record.
51
+ projectionSchedules: number;
52
+ projectionFlushes: number;
53
+ // A sticky wrapper rebuilt loses its measured layout and costs a native round trip; reuse is the
54
+ // fixed path. Creates climbing during a plain scroll is the regression this pair pins.
55
+ stickyWrapperCreates: number;
56
+ stickyWrapperReuses: number;
57
+ }
58
+
59
+ type IAngularCounter = keyof IAngularProfile;
60
+
61
+ function createCounters(): IAngularProfile {
62
+ return {
63
+ cdPasses: 0,
64
+ viewsChecked: 0,
65
+ rendererWrites: 0,
66
+ nodesCreated: 0,
67
+ nodesInserted: 0,
68
+ nodesRemoved: 0,
69
+ styleChecks: 0,
70
+ styleMarks: 0,
71
+ listChecks: 0,
72
+ listRecomputes: 0,
73
+ listMarks: 0,
74
+ scrollTicks: 0,
75
+ outletCreates: 0,
76
+ outletUpdates: 0,
77
+ outletDestroys: 0,
78
+ projectionSchedules: 0,
79
+ projectionFlushes: 0,
80
+ stickyWrapperCreates: 0,
81
+ stickyWrapperReuses: 0,
82
+ };
83
+ }
84
+
85
+ let counters = createCounters();
86
+
87
+ export function countAngular(counter: IAngularCounter): void {
88
+ counters[counter] += 1;
89
+ }
90
+
91
+ // Reading zeroes the counters, so a sampler on an interval gets disjoint windows rather than a
92
+ // growing total - same contract as readCommitProfile().
93
+ export function readAngularProfile(): IAngularProfile {
94
+ const snapshot = counters;
95
+ counters = createCounters();
96
+ return snapshot;
97
+ }
98
+
99
+ // Which prop names the renderer actually rewrites, and which host tags get created. Off by default
100
+ // because it allocates per write; the probe screen turns it on for one sample. A write nothing
101
+ // asked for is the whole diagnosis, and the prop NAME is what points at the binding responsible.
102
+ let isDetailEnabled = false;
103
+ const writesByProp = new Map<string, number>();
104
+ const createsByTag = new Map<string, number>();
105
+
106
+ export function setAngularProfileDetail(enabled: boolean): void {
107
+ isDetailEnabled = enabled;
108
+ if (!enabled) {
109
+ writesByProp.clear();
110
+ createsByTag.clear();
111
+ }
112
+ }
113
+
114
+ export function noteAngularWrite(propName: string): void {
115
+ if (!isDetailEnabled) return;
116
+ writesByProp.set(propName, (writesByProp.get(propName) ?? 0) + 1);
117
+ }
118
+
119
+ export function noteAngularCreate(tagName: string): void {
120
+ if (!isDetailEnabled) return;
121
+ createsByTag.set(tagName, (createsByTag.get(tagName) ?? 0) + 1);
122
+ }
123
+
124
+ export interface IAngularProfileDetail {
125
+ writesByProp: readonly (readonly [string, number])[];
126
+ createsByTag: readonly (readonly [string, number])[];
127
+ }
128
+
129
+ // Sorted hottest-first and read-and-cleared, so a sample names its own top offenders.
130
+ export function readAngularProfileDetail(): IAngularProfileDetail {
131
+ const detail = {
132
+ writesByProp: [...writesByProp.entries()].sort((a, b) => b[1] - a[1]),
133
+ createsByTag: [...createsByTag.entries()].sort((a, b) => b[1] - a[1]),
134
+ };
135
+ writesByProp.clear();
136
+ createsByTag.clear();
137
+ return detail;
138
+ }
@@ -0,0 +1,109 @@
1
+ // Whether anything downstream actually wants a gated accessibility event, passed from a wrapper
2
+ // component to the inner component that owns the gate.
3
+ //
4
+ // THE PROBLEM. Six Fabric events fire only if a BOOLEAN prop reaches the payload
5
+ // (`.claude/rules/fabric-boolean-event-gates.md`), and the flag follows the VALUE: a handler
6
+ // present means `true`, absent means the key is deleted. Every component that owns such a gate
7
+ // therefore writes it only while its own `@Output()` is `.observed`.
8
+ //
9
+ // Angular has no conditional template binding, so a wrapper rendering an inner component must write
10
+ // `(accessibilityAction)="accessibilityAction.emit($event)"` unconditionally — which subscribes,
11
+ // which makes the inner's `.observed` permanently true, which lights the flag on EVERY instance
12
+ // whether or not any app listens. Fixing the inner component does nothing: the wrapper is the
13
+ // subscriber. The chain is up to four deep here (SectionList -> VirtualizedSectionList ->
14
+ // VirtualizedList -> ScrollView), and every middle link has the same problem for the same reason.
15
+ //
16
+ // WHY THIS IS DI AND NOT AN `@Input`. An input is public API. An app could write
17
+ // `[gatedEvents]="…"` on a `Pressable` and silently switch its accessibility events OFF — a change
18
+ // nothing in this repo could detect and nothing on screen would show. A `viewProviders` entry is
19
+ // not part of the component's binding surface, so an app cannot reach it by accident.
20
+ //
21
+ // WHY `viewProviders` AND NOT `providers` — measured, both arms, 2026-09-02:
22
+ //
23
+ // viewProviders the wrapper's OWN template sees it, projected content does NOT
24
+ // providers both see it
25
+ //
26
+ // The second half is what matters: an app writing `<TouchableOpacity><Pressable/></TouchableOpacity>`
27
+ // projects its own Pressable INTO the wrapper, and that Pressable must behave exactly as it would
28
+ // standing alone. Under `providers` it would inherit the wrapper's demand and the debt would just
29
+ // move one level down.
30
+ //
31
+ // Verified on both rungs — JIT through `mount()`, and again by executing the artifact from a real
32
+ // `ngc --compilationMode partial` + `babel-linker.cjs` chain, because `viewProviders` compiles into
33
+ // the component definition (`ɵɵProvidersFeature`) and this repo has a recorded case of JIT and AOT
34
+ // disagreeing on a compiled binding (`.claude/rules/test-harness-false-greens.md` §21/§21a). Both
35
+ // arms reported the same four cells.
36
+ import {
37
+ EventEmitter,
38
+ InjectionToken,
39
+ forwardRef,
40
+ inject,
41
+ } from '@angular/core';
42
+ import type { Provider, Type } from '@angular/core';
43
+ import type { ISymbioteEvent } from '@symbiote-native/engine';
44
+
45
+ // The four `on*`-gated accessibility events every wrapper forwards. Not the full gate list from the
46
+ // engine: `layout` and `textLayout` are never forwarded wrapper-to-wrapper, and
47
+ // KeyboardAvoidingView READS its own `onLayout` internally, so gating that one on a subscriber
48
+ // would break inset-following (`.claude/rules/keyboard-avoiding-view-rn-contract.md`).
49
+ export const GATED_ACCESSIBILITY_EVENTS = [
50
+ 'accessibilityAction',
51
+ 'accessibilityTap',
52
+ 'magicTap',
53
+ 'accessibilityEscape',
54
+ ] as const;
55
+
56
+ export type IGatedAccessibilityEvent =
57
+ (typeof GATED_ACCESSIBILITY_EVENTS)[number];
58
+
59
+ export interface IGateDemand {
60
+ wantsGate(name: IGatedAccessibilityEvent): boolean;
61
+ }
62
+
63
+ export const GATE_DEMAND = new InjectionToken<IGateDemand>(
64
+ 'symbiote.gate-demand',
65
+ );
66
+
67
+ /**
68
+ * Declare a wrapper as the demand for everything in its own template.
69
+ *
70
+ * Takes a THUNK rather than the class: the entry sits in the decorator of the very class it names,
71
+ * so the reference has to be late.
72
+ */
73
+ export function provideGateDemand(get: () => Type<IGateDemand>): Provider {
74
+ return { provide: GATE_DEMAND, useExisting: forwardRef(get) };
75
+ }
76
+
77
+ /**
78
+ * For a component that OWNS a gate — the innermost one, which writes the flag.
79
+ */
80
+ export function injectGateDemand(): IGateDemand | null {
81
+ return inject(GATE_DEMAND, { optional: true });
82
+ }
83
+
84
+ /**
85
+ * For a component that PROVIDES demand and is itself wrapped.
86
+ *
87
+ * `skipSelf` is not optional here: a component resolves its own `viewProviders`, so without it the
88
+ * lookup reaches this class's own `useExisting` entry and Angular throws NG0200 at construction.
89
+ * Loud rather than silent, but the shape is easy to get wrong, which is why the two lookups are
90
+ * separate functions instead of one with a flag.
91
+ */
92
+ export function injectGateDemandAbove(): IGateDemand | null {
93
+ return inject(GATE_DEMAND, { optional: true, skipSelf: true });
94
+ }
95
+
96
+ /**
97
+ * The one expression both roles evaluate.
98
+ *
99
+ * A demand from above OVERRIDES the local emitter, and that override is the whole mechanism: a
100
+ * middle link's own emitter is `.observed` precisely because the level above bound it, so answering
101
+ * from it would report "wanted" for a chain nobody subscribed to.
102
+ */
103
+ export function gateWanted(
104
+ above: IGateDemand | null,
105
+ name: IGatedAccessibilityEvent,
106
+ own: EventEmitter<ISymbioteEvent>,
107
+ ): boolean {
108
+ return above !== null ? above.wantsGate(name) : own.observed;
109
+ }
@@ -13,14 +13,19 @@
13
13
  // An uncommitted or unknown input surfaces as null.
14
14
 
15
15
  import { ElementRef } from '@angular/core';
16
- import { getNativeTag, isSymbioteNode, type ISymbioteNode } from '@symbiote-native/engine';
16
+ import {
17
+ getNativeTag,
18
+ isSymbioteNode,
19
+ type ISymbioteNode,
20
+ } from '@symbiote-native/engine';
17
21
 
18
22
  export type { IHostInstance } from '@symbiote-native/engine';
19
23
 
20
24
  function resolveHostNode(candidate: unknown): ISymbioteNode | null {
21
25
  if (candidate === null || candidate === undefined) return null;
22
26
  if (isSymbioteNode(candidate)) return candidate;
23
- if (candidate instanceof ElementRef) return resolveHostNode(candidate.nativeElement);
27
+ if (candidate instanceof ElementRef)
28
+ return resolveHostNode(candidate.nativeElement);
24
29
  const maybeHost = candidate as { nativeElement?: unknown };
25
30
  if (typeof maybeHost.nativeElement !== 'undefined') {
26
31
  return resolveHostNode(maybeHost.nativeElement);
@@ -29,7 +34,8 @@ function resolveHostNode(candidate: unknown): ISymbioteNode | null {
29
34
  }
30
35
 
31
36
  export function findNodeHandle(componentOrHandle: unknown): number | null {
32
- if (componentOrHandle === null || componentOrHandle === undefined) return null;
37
+ if (componentOrHandle === null || componentOrHandle === undefined)
38
+ return null;
33
39
  if (typeof componentOrHandle === 'number') return componentOrHandle;
34
40
  const node = resolveHostNode(componentOrHandle);
35
41
  return node ? (getNativeTag(node) ?? null) : null;
package/src/index.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  // SEAM SCAFFOLD: mount/unmount + the renderer seam + host intrinsic selectors. Full RN-like
6
6
  // composed components still flow through the shared @symbiote-native/components bridge.
7
7
 
8
+ import './register';
9
+
8
10
  export {
9
11
  ActivityIndicator,
10
12
  anchorHostStyle,
@@ -61,6 +63,16 @@ export {
61
63
  AnimatedText,
62
64
  AnimatedView,
63
65
  } from './modules/animated';
66
+ // Exported even though Angular's version maps only the four primitives (View/Text/Image/
67
+ // ScrollView) onto the pre-authored wrappers above and THROWS on anything else — there is no JIT
68
+ // under AOT/Metro, so it cannot synthesize a wrapper at runtime the way the other four adapters
69
+ // do. It ships anyway because portable code (`createAnimatedComponent(View)`) then compiles and
70
+ // runs identically on all five adapters, and the one case Angular cannot serve fails with a
71
+ // message naming the fix (author a standalone @Component over AnimatedComponentBase) instead of
72
+ // failing as a missing export, which names nothing. Found by the barrel audit in
73
+ // `.claude/rules/adapter-parity-audit.md`: it was implemented and tested here all along and simply
74
+ // never reached this file, so `@symbiote-native/angular` consumers could not reach it at all.
75
+ export { createAnimatedComponent } from './modules/animated';
64
76
  export type {
65
77
  IActivityIndicatorProps,
66
78
  IAngularImageBackgroundProps,
@@ -135,7 +147,12 @@ export { DescriptorOutlet } from './descriptor-to-angular';
135
147
  // idiom) parameterized by an `@Input()`, rather than a factory returning fresh components per
136
148
  // call.
137
149
  export { PortalDirective, PortalOutletDirective } from './create-portal';
138
- export { createTunnel, TunnelInDirective, TunnelOut, type ITunnelStore } from './create-tunnel';
150
+ export {
151
+ createTunnel,
152
+ TunnelInDirective,
153
+ TunnelOut,
154
+ type ITunnelStore,
155
+ } from './create-tunnel';
139
156
  export { SymbioteRenderer, SymbioteRendererFactory } from './renderer';
140
157
  // registerComposedComponent lives in the dependency-free leaf ./anchor-host-registry, NOT in the
141
158
  // require-cyclic ./renderer — see the leaf header. The babel-register-composed plugin injects the
@@ -258,7 +275,11 @@ export {
258
275
  setNativeViewConfigSource,
259
276
  StyleSheet,
260
277
  } from '@symbiote-native/engine';
261
- export type { IRootTag, ISymbioteEvent, ISymbioteNode } from '@symbiote-native/engine';
278
+ export type {
279
+ IRootTag,
280
+ ISymbioteEvent,
281
+ ISymbioteNode,
282
+ } from '@symbiote-native/engine';
262
283
  // The agnostic style / platform / view-config types behind the values above. Angular takes its
263
284
  // component props as @Input()s and exports no per-component prop type, but an app still needs
264
285
  // these to type a style object or a Platform.select spec.
@@ -288,3 +309,13 @@ export type {
288
309
  IPressableAndroidRippleConfig,
289
310
  IImageStatics,
290
311
  } from '@symbiote-native/components';
312
+
313
+ // Diagnostics: per-window Angular-side counters (change-detection passes, renderer writes, list
314
+ // recomputes, cell views). The engine's readCommitProfile() prices the commit; these say how many
315
+ // times anything ran at all - the number that exposes a change-detection free-run.
316
+ export {
317
+ readAngularProfile,
318
+ readAngularProfileDetail,
319
+ setAngularProfileDetail,
320
+ } from './diagnostics';
321
+ export type { IAngularProfile, IAngularProfileDetail } from './diagnostics';
@@ -53,8 +53,10 @@ import {
53
53
  IMAGE_INPUTS,
54
54
  IMAGE_OUTPUTS,
55
55
  ImageBase,
56
+ isImageEventCallback,
56
57
  resolveImageProps,
57
58
  } from '../../components/image/shared';
59
+ import type { IGatedAccessibilityEvent } from '../../gate-demand';
58
60
  import { SectionList } from '../../components/section-list';
59
61
  import { AnimatedLeafBinder } from './animated-leaf-binder';
60
62
 
@@ -69,7 +71,11 @@ const PASSTHROUGH_PROP = 'passthroughAnimatedPropExplicitValues';
69
71
  // plus static host props to forward (the React/Vue `rest`). Each concrete @Component re-lists
70
72
  // these (Angular's inputs-on-base convention, mirroring Switch/Image).
71
73
  export const ANIMATED_INPUTS = ['style', 'animatedProps', PASSTHROUGH_PROP];
72
- export const ANIMATED_IMAGE_INPUTS = [...IMAGE_INPUTS, 'animatedProps', PASSTHROUGH_PROP];
74
+ export const ANIMATED_IMAGE_INPUTS = [
75
+ ...IMAGE_INPUTS,
76
+ 'animatedProps',
77
+ PASSTHROUGH_PROP,
78
+ ];
73
79
 
74
80
  // The shared lifecycle for every animated wrapper. Concrete subclasses add ONLY a @Component
75
81
  // decorator (selector + the host-primitive template) — no behavior. Plain fields (no @Input
@@ -92,7 +98,8 @@ export abstract class AnimatedComponentBase
92
98
 
93
99
  // The directive sitting on the inner host primitive; its `node` is the committed SymbioteNode
94
100
  // the leaf binds to. Inherited by the decorated subclass (Angular collects base-class queries).
95
- @ViewChild(SymbioteHostPropsDirective) private hostDirective?: SymbioteHostPropsDirective;
101
+ @ViewChild(SymbioteHostPropsDirective)
102
+ private hostDirective?: SymbioteHostPropsDirective;
96
103
 
97
104
  // The leaf-lifecycle orchestration (build/attach/swap/bind/detach an AnimatedProps leaf) is a
98
105
  // Pure Fabrication shared with AnimatedImage, which cannot extend this class (it must extend
@@ -110,10 +117,14 @@ export abstract class AnimatedComponentBase
110
117
  // array, which the commit layer flattens) so the ShadowTree carries the current transform.
111
118
  get reducedProps(): Record<string, unknown> {
112
119
  const reduced = reduceProps(this.mergedProps());
113
- const passthroughStyle = readPassthroughStyle(this.passthroughAnimatedPropExplicitValues);
120
+ const passthroughStyle = readPassthroughStyle(
121
+ this.passthroughAnimatedPropExplicitValues,
122
+ );
114
123
  if (passthroughStyle !== undefined) {
115
124
  reduced['style'] =
116
- reduced['style'] === undefined ? passthroughStyle : [reduced['style'], passthroughStyle];
125
+ reduced['style'] === undefined
126
+ ? passthroughStyle
127
+ : [reduced['style'], passthroughStyle];
117
128
  }
118
129
  reduced['style'] = [this.anchorStyle(), reduced['style']];
119
130
  return reduced;
@@ -162,7 +173,8 @@ export abstract class AnimatedComponentBase
162
173
  const hasPassthroughAnimatedValues =
163
174
  this.passthroughAnimatedPropExplicitValues !== null &&
164
175
  this.passthroughAnimatedPropExplicitValues !== undefined;
165
- const wantsNative = hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
176
+ const wantsNative =
177
+ hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
166
178
  this.leafBinder.reconcile(props, wantsNative);
167
179
  }
168
180
 
@@ -171,7 +183,9 @@ export abstract class AnimatedComponentBase
171
183
  // never disagree.
172
184
  private mergedProps(): Record<string, unknown> {
173
185
  const base = this.animatedProps ?? {};
174
- return this.style === undefined ? { ...base } : { ...base, style: this.style };
186
+ return this.style === undefined
187
+ ? { ...base }
188
+ : { ...base, style: this.style };
175
189
  }
176
190
 
177
191
  // The committed host node held by IDENTITY. resolveHostNode unwraps an imperative scroll
@@ -186,7 +200,9 @@ export abstract class AnimatedComponentBase
186
200
  @Component({
187
201
  selector: 'AnimatedView, symbiote-animated-view',
188
202
  standalone: true,
189
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
203
+ hostDirectives: [
204
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
205
+ ],
190
206
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
191
207
  imports: [SymbioteHostPropsDirective, ViewHost],
192
208
  inputs: ANIMATED_INPUTS,
@@ -202,7 +218,9 @@ export class AnimatedView extends AnimatedComponentBase {}
202
218
  @Component({
203
219
  selector: 'AnimatedText, symbiote-animated-text',
204
220
  standalone: true,
205
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
221
+ hostDirectives: [
222
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
223
+ ],
206
224
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
207
225
  imports: [SymbioteHostPropsDirective, TextHost],
208
226
  inputs: ANIMATED_INPUTS,
@@ -218,7 +236,9 @@ export class AnimatedText extends AnimatedComponentBase {}
218
236
  @Component({
219
237
  selector: 'AnimatedImage, symbiote-animated-image',
220
238
  standalone: true,
221
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
239
+ hostDirectives: [
240
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
241
+ ],
222
242
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
223
243
  imports: [SymbioteHostPropsDirective, ImageHost],
224
244
  inputs: ANIMATED_IMAGE_INPUTS,
@@ -226,10 +246,6 @@ export class AnimatedText extends AnimatedComponentBase {}
226
246
  template: `
227
247
  <symbiote-image
228
248
  [symbioteHostProps]="animatedImageProps"
229
- (accessibilityAction)="handleAccessibilityAction($event)"
230
- (accessibilityTap)="handleAccessibilityTap($event)"
231
- (magicTap)="handleMagicTap($event)"
232
- (accessibilityEscape)="handleAccessibilityEscape($event)"
233
249
  (loadStart)="handleLoadStart($event)"
234
250
  (load)="handleLoad($event)"
235
251
  (loadEnd)="handleLoadEnd($event)"
@@ -242,7 +258,10 @@ export class AnimatedText extends AnimatedComponentBase {}
242
258
  `,
243
259
  changeDetection: ChangeDetectionStrategy.OnPush,
244
260
  })
245
- export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges, OnDestroy {
261
+ export class AnimatedImage
262
+ extends ImageBase
263
+ implements AfterViewInit, OnChanges, OnDestroy
264
+ {
246
265
  animatedProps: Record<string, unknown> | undefined;
247
266
  passthroughAnimatedPropExplicitValues: unknown;
248
267
 
@@ -250,7 +269,8 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
250
269
  // comment on AnimatedComponentBase.elementRef.
251
270
  private readonly elementRef = inject(ElementRef);
252
271
 
253
- @ViewChild(SymbioteHostPropsDirective) private hostDirective?: SymbioteHostPropsDirective;
272
+ @ViewChild(SymbioteHostPropsDirective)
273
+ private hostDirective?: SymbioteHostPropsDirective;
254
274
 
255
275
  // Same Pure Fabrication AnimatedComponentBase holds — see its matching comment. AnimatedImage
256
276
  // can't extend AnimatedComponentBase (it must extend ImageBase, see the file header), so it
@@ -261,15 +281,40 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
261
281
  );
262
282
  private viewReady = false;
263
283
 
284
+ private gateInto(
285
+ props: Record<string, unknown>,
286
+ name: IGatedAccessibilityEvent,
287
+ key: string,
288
+ ): void {
289
+ const carried = props[key];
290
+ props[key] = this.gatedAccessibilityHandler(
291
+ name,
292
+ isImageEventCallback(carried) ? carried : undefined,
293
+ );
294
+ }
295
+
264
296
  get animatedImageProps(): Record<string, unknown> {
265
297
  const reduced = reduceProps(this.mergedProps());
266
- const passthroughStyle = readPassthroughStyle(this.passthroughAnimatedPropExplicitValues);
298
+ const passthroughStyle = readPassthroughStyle(
299
+ this.passthroughAnimatedPropExplicitValues,
300
+ );
267
301
  if (passthroughStyle !== undefined) {
268
302
  reduced['style'] =
269
- reduced['style'] === undefined ? passthroughStyle : [reduced['style'], passthroughStyle];
303
+ reduced['style'] === undefined
304
+ ? passthroughStyle
305
+ : [reduced['style'], passthroughStyle];
270
306
  }
271
307
  const resolved = resolveImageProps(reduced);
272
308
  resolved['style'] = [anchorHostStyle(this.elementRef), resolved['style']];
309
+ // The four gated accessibility events, which used to be template bindings on the host tag and
310
+ // therefore lit their Fabric flags on every AnimatedImage in the app
311
+ // (`.claude/rules/fabric-boolean-event-gates.md`). Routed through the base's gate instead, so
312
+ // both channels still reach the app — the `onX` callback carried in the animated prop bag, and
313
+ // the `@Output()` an app may bind on <AnimatedImage> (IMAGE_OUTPUTS declares all four).
314
+ this.gateInto(resolved, 'accessibilityAction', 'onAccessibilityAction');
315
+ this.gateInto(resolved, 'accessibilityTap', 'onAccessibilityTap');
316
+ this.gateInto(resolved, 'magicTap', 'onMagicTap');
317
+ this.gateInto(resolved, 'accessibilityEscape', 'onAccessibilityEscape');
273
318
  return resolved;
274
319
  }
275
320
 
@@ -311,7 +356,8 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
311
356
  const hasPassthroughAnimatedValues =
312
357
  this.passthroughAnimatedPropExplicitValues !== null &&
313
358
  this.passthroughAnimatedPropExplicitValues !== undefined;
314
- const wantsNative = hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
359
+ const wantsNative =
360
+ hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
315
361
  this.leafBinder.reconcile(props, wantsNative);
316
362
  }
317
363
 
@@ -332,7 +378,9 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
332
378
  @Component({
333
379
  selector: 'AnimatedScrollView, symbiote-animated-scroll-view',
334
380
  standalone: true,
335
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
381
+ hostDirectives: [
382
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
383
+ ],
336
384
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
337
385
  imports: [SymbioteHostPropsDirective, ScrollViewHost],
338
386
  inputs: ANIMATED_INPUTS,
@@ -23,7 +23,9 @@ export type IComponentProvider = () => Type<unknown>;
23
23
  // a pre-authored standalone @Component whose template renders `<ng-content>` — the Angular idiom
24
24
  // for "render my children" (mount() projects the root's host node into it, the twin of React's
25
25
  // createElement(Wrapper, null, rootElement)).
26
- export type IWrapperComponentProvider = (appParameters: IAppParameters) => Type<unknown>;
26
+ export type IWrapperComponentProvider = (
27
+ appParameters: IAppParameters,
28
+ ) => Type<unknown>;
27
29
 
28
30
  function runnableFor(
29
31
  componentProvider: IComponentProvider,
@@ -5,7 +5,12 @@
5
5
  // ngOnChanges on mount + every prop change. Imperative statics are attached to the component
6
6
  // object the same way RN does.
7
7
 
8
- import { Component, Input, type OnChanges, type SimpleChanges } from '@angular/core';
8
+ import {
9
+ Component,
10
+ Input,
11
+ type OnChanges,
12
+ type SimpleChanges,
13
+ } from '@angular/core';
9
14
  import {
10
15
  applyStatusBarProps,
11
16
  statusBarImperative,
@@ -46,12 +51,16 @@ class StatusBarComponent implements OnChanges {
46
51
  }
47
52
  }
48
53
 
49
- const StatusBarWithStatics = Object.assign(StatusBarComponent, statusBarImperative);
54
+ const StatusBarWithStatics = Object.assign(
55
+ StatusBarComponent,
56
+ statusBarImperative,
57
+ );
50
58
 
51
59
  Object.defineProperty(StatusBarWithStatics, 'currentHeight', {
52
60
  get: statusBarCurrentHeight,
53
61
  enumerable: true,
54
62
  });
55
63
 
56
- export const StatusBar: typeof StatusBarWithStatics & { readonly currentHeight?: number } =
57
- StatusBarWithStatics;
64
+ export const StatusBar: typeof StatusBarWithStatics & {
65
+ readonly currentHeight?: number;
66
+ } = StatusBarWithStatics;