@symbiote-native/angular 0.7.0 → 0.8.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.
- package/README.md +3 -3
- package/build/angular/bootstrap.js +1 -1
- package/build/angular/bootstrap.js.map +1 -1
- package/build/angular/components/activity-indicator/index.android.js +7 -2
- package/build/angular/components/activity-indicator/index.android.js.map +1 -1
- package/build/angular/components/activity-indicator/index.ios.js +3 -1
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
- package/build/angular/components/activity-indicator/shared.d.ts +1 -1
- package/build/angular/components/activity-indicator/shared.js +1 -1
- package/build/angular/components/activity-indicator/shared.js.map +1 -1
- package/build/angular/components/button.js +13 -5
- package/build/angular/components/button.js.map +1 -1
- package/build/angular/components/flat-list/index.d.ts +1 -1
- package/build/angular/components/flat-list/index.js +47 -14
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image/index.android.js +4 -2
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.js +4 -2
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.js +18 -6
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.js +22 -8
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.js +7 -2
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +1 -0
- package/build/angular/components/keyboard-avoiding-view/index.js +39 -9
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.js +7 -2
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +1 -1
- package/build/angular/components/pressable/index.js +6 -2
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.js +3 -1
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.js +3 -1
- package/build/angular/components/safe-area-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/index.android.js +44 -14
- package/build/angular/components/scroll-view/index.android.js.map +1 -1
- package/build/angular/components/scroll-view/index.ios.js +13 -5
- package/build/angular/components/scroll-view/index.ios.js.map +1 -1
- package/build/angular/components/scroll-view/projection.d.ts +32 -0
- package/build/angular/components/scroll-view/projection.js +212 -47
- package/build/angular/components/scroll-view/projection.js.map +1 -1
- package/build/angular/components/scroll-view/shared.js +28 -7
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/scroll-view/sticky-header.js +7 -3
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +6 -1
- package/build/angular/components/section-list/index.js +63 -18
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.js +17 -3
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.js +17 -3
- package/build/angular/components/switch/index.ios.js.map +1 -1
- package/build/angular/components/switch/shared.d.ts +1 -1
- package/build/angular/components/switch/shared.js +5 -2
- package/build/angular/components/switch/shared.js.map +1 -1
- package/build/angular/components/text-input.d.ts +1 -1
- package/build/angular/components/text-input.js +25 -5
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +29 -7
- package/build/angular/components/touchable/index.js +246 -69
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.js +2 -2
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
- package/build/angular/components/virtualized-list/directives.js +4 -0
- package/build/angular/components/virtualized-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +5 -2
- package/build/angular/components/virtualized-list/index.js +197 -60
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.d.ts +18 -1
- package/build/angular/components/virtualized-section-list/index.js +90 -21
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +1 -1
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.js +5 -1
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +3 -1
- package/build/angular/descriptor-to-angular/index.js.map +1 -1
- package/build/angular/diagnostics.d.ts +33 -0
- package/build/angular/diagnostics.js +82 -0
- package/build/angular/diagnostics.js.map +1 -0
- package/build/angular/host-instance/index.js +1 -1
- package/build/angular/host-instance/index.js.map +1 -1
- package/build/angular/index.d.ts +5 -2
- package/build/angular/index.js +15 -1
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.js +27 -9
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/app-registry/index.js.map +1 -1
- package/build/angular/modules/status-bar/index.js +1 -1
- package/build/angular/modules/status-bar/index.js.map +1 -1
- package/build/angular/primitives/index.d.ts +19 -2
- package/build/angular/primitives/index.js +39 -3
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +9 -1
- package/build/angular/primitives/shared.js +49 -3
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +48 -5
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +1 -0
- package/build/angular/renderer/index.js +37 -2
- package/build/angular/renderer/index.js.map +1 -1
- package/build/angular/services/color-scheme.service.js.map +1 -1
- package/build/angular/services/window-dimensions.service.js +1 -1
- package/build/angular/services/window-dimensions.service.js.map +1 -1
- package/package.json +5 -5
- package/src/bootstrap.ts +4 -1
- package/src/components/activity-indicator/index.android.ts +7 -2
- package/src/components/activity-indicator/index.ios.ts +3 -1
- package/src/components/activity-indicator/shared.ts +32 -13
- package/src/components/button.ts +19 -6
- package/src/components/flat-list/index.ts +91 -29
- package/src/components/image/index.android.ts +9 -2
- package/src/components/image/index.ios.ts +9 -2
- package/src/components/image/shared.ts +58 -19
- package/src/components/image-background.ts +29 -10
- package/src/components/input-accessory-view/index.ts +27 -10
- package/src/components/keyboard-avoiding-view/index.ts +55 -15
- package/src/components/modal/index.ts +22 -8
- package/src/components/pressable/index.ts +51 -21
- package/src/components/refresh-control/index.ts +22 -8
- package/src/components/safe-area-view/index.ts +16 -7
- package/src/components/scroll-view/index.android.ts +31 -9
- package/src/components/scroll-view/index.ios.ts +17 -4
- package/src/components/scroll-view/projection.ts +290 -63
- package/src/components/scroll-view/shared.ts +93 -26
- package/src/components/scroll-view/sticky-header.ts +25 -8
- package/src/components/section-list/index.ts +61 -19
- package/src/components/switch/index.android.ts +14 -3
- package/src/components/switch/index.ios.ts +14 -3
- package/src/components/switch/shared.ts +15 -4
- package/src/components/text-input.ts +45 -13
- package/src/components/touchable/index.ts +296 -72
- package/src/components/touchable-native-feedback/index.ts +32 -10
- package/src/components/virtualized-list/directives.ts +19 -5
- package/src/components/virtualized-list/index.ts +274 -72
- package/src/components/virtualized-section-list/directives.ts +6 -3
- package/src/components/virtualized-section-list/index.ts +146 -34
- package/src/components.ts +4 -1
- package/src/create-portal/index.ts +8 -2
- package/src/create-tunnel/index.ts +3 -1
- package/src/descriptor-to-angular/index.ts +29 -8
- package/src/diagnostics.ts +138 -0
- package/src/host-instance/index.ts +9 -3
- package/src/index.ts +31 -2
- package/src/modules/animated/create-animated-component.ts +44 -15
- package/src/modules/app-registry/index.ts +3 -1
- package/src/modules/status-bar/index.ts +13 -4
- package/src/primitives/index.ts +46 -2
- package/src/primitives/shared.ts +70 -9
- package/src/render/index.ts +56 -6
- package/src/renderer/index.ts +76 -14
- package/src/services/color-scheme.service.ts +3 -1
- package/src/services/window-dimensions.service.ts +11 -4
package/src/index.ts
CHANGED
|
@@ -61,6 +61,16 @@ export {
|
|
|
61
61
|
AnimatedText,
|
|
62
62
|
AnimatedView,
|
|
63
63
|
} from './modules/animated';
|
|
64
|
+
// Exported even though Angular's version maps only the four primitives (View/Text/Image/
|
|
65
|
+
// ScrollView) onto the pre-authored wrappers above and THROWS on anything else — there is no JIT
|
|
66
|
+
// under AOT/Metro, so it cannot synthesize a wrapper at runtime the way the other four adapters
|
|
67
|
+
// do. It ships anyway because portable code (`createAnimatedComponent(View)`) then compiles and
|
|
68
|
+
// runs identically on all five adapters, and the one case Angular cannot serve fails with a
|
|
69
|
+
// message naming the fix (author a standalone @Component over AnimatedComponentBase) instead of
|
|
70
|
+
// failing as a missing export, which names nothing. Found by the barrel audit in
|
|
71
|
+
// `.claude/rules/adapter-parity-audit.md`: it was implemented and tested here all along and simply
|
|
72
|
+
// never reached this file, so `@symbiote-native/angular` consumers could not reach it at all.
|
|
73
|
+
export { createAnimatedComponent } from './modules/animated';
|
|
64
74
|
export type {
|
|
65
75
|
IActivityIndicatorProps,
|
|
66
76
|
IAngularImageBackgroundProps,
|
|
@@ -135,7 +145,12 @@ export { DescriptorOutlet } from './descriptor-to-angular';
|
|
|
135
145
|
// idiom) parameterized by an `@Input()`, rather than a factory returning fresh components per
|
|
136
146
|
// call.
|
|
137
147
|
export { PortalDirective, PortalOutletDirective } from './create-portal';
|
|
138
|
-
export {
|
|
148
|
+
export {
|
|
149
|
+
createTunnel,
|
|
150
|
+
TunnelInDirective,
|
|
151
|
+
TunnelOut,
|
|
152
|
+
type ITunnelStore,
|
|
153
|
+
} from './create-tunnel';
|
|
139
154
|
export { SymbioteRenderer, SymbioteRendererFactory } from './renderer';
|
|
140
155
|
// registerComposedComponent lives in the dependency-free leaf ./anchor-host-registry, NOT in the
|
|
141
156
|
// require-cyclic ./renderer — see the leaf header. The babel-register-composed plugin injects the
|
|
@@ -258,7 +273,11 @@ export {
|
|
|
258
273
|
setNativeViewConfigSource,
|
|
259
274
|
StyleSheet,
|
|
260
275
|
} from '@symbiote-native/engine';
|
|
261
|
-
export type {
|
|
276
|
+
export type {
|
|
277
|
+
IRootTag,
|
|
278
|
+
ISymbioteEvent,
|
|
279
|
+
ISymbioteNode,
|
|
280
|
+
} from '@symbiote-native/engine';
|
|
262
281
|
// The agnostic style / platform / view-config types behind the values above. Angular takes its
|
|
263
282
|
// component props as @Input()s and exports no per-component prop type, but an app still needs
|
|
264
283
|
// these to type a style object or a Platform.select spec.
|
|
@@ -288,3 +307,13 @@ export type {
|
|
|
288
307
|
IPressableAndroidRippleConfig,
|
|
289
308
|
IImageStatics,
|
|
290
309
|
} from '@symbiote-native/components';
|
|
310
|
+
|
|
311
|
+
// Diagnostics: per-window Angular-side counters (change-detection passes, renderer writes, list
|
|
312
|
+
// recomputes, cell views). The engine's readCommitProfile() prices the commit; these say how many
|
|
313
|
+
// times anything ran at all - the number that exposes a change-detection free-run.
|
|
314
|
+
export {
|
|
315
|
+
readAngularProfile,
|
|
316
|
+
readAngularProfileDetail,
|
|
317
|
+
setAngularProfileDetail,
|
|
318
|
+
} from './diagnostics';
|
|
319
|
+
export type { IAngularProfile, IAngularProfileDetail } from './diagnostics';
|
|
@@ -69,7 +69,11 @@ const PASSTHROUGH_PROP = 'passthroughAnimatedPropExplicitValues';
|
|
|
69
69
|
// plus static host props to forward (the React/Vue `rest`). Each concrete @Component re-lists
|
|
70
70
|
// these (Angular's inputs-on-base convention, mirroring Switch/Image).
|
|
71
71
|
export const ANIMATED_INPUTS = ['style', 'animatedProps', PASSTHROUGH_PROP];
|
|
72
|
-
export const ANIMATED_IMAGE_INPUTS = [
|
|
72
|
+
export const ANIMATED_IMAGE_INPUTS = [
|
|
73
|
+
...IMAGE_INPUTS,
|
|
74
|
+
'animatedProps',
|
|
75
|
+
PASSTHROUGH_PROP,
|
|
76
|
+
];
|
|
73
77
|
|
|
74
78
|
// The shared lifecycle for every animated wrapper. Concrete subclasses add ONLY a @Component
|
|
75
79
|
// decorator (selector + the host-primitive template) — no behavior. Plain fields (no @Input
|
|
@@ -92,7 +96,8 @@ export abstract class AnimatedComponentBase
|
|
|
92
96
|
|
|
93
97
|
// The directive sitting on the inner host primitive; its `node` is the committed SymbioteNode
|
|
94
98
|
// the leaf binds to. Inherited by the decorated subclass (Angular collects base-class queries).
|
|
95
|
-
@ViewChild(SymbioteHostPropsDirective)
|
|
99
|
+
@ViewChild(SymbioteHostPropsDirective)
|
|
100
|
+
private hostDirective?: SymbioteHostPropsDirective;
|
|
96
101
|
|
|
97
102
|
// The leaf-lifecycle orchestration (build/attach/swap/bind/detach an AnimatedProps leaf) is a
|
|
98
103
|
// Pure Fabrication shared with AnimatedImage, which cannot extend this class (it must extend
|
|
@@ -110,10 +115,14 @@ export abstract class AnimatedComponentBase
|
|
|
110
115
|
// array, which the commit layer flattens) so the ShadowTree carries the current transform.
|
|
111
116
|
get reducedProps(): Record<string, unknown> {
|
|
112
117
|
const reduced = reduceProps(this.mergedProps());
|
|
113
|
-
const passthroughStyle = readPassthroughStyle(
|
|
118
|
+
const passthroughStyle = readPassthroughStyle(
|
|
119
|
+
this.passthroughAnimatedPropExplicitValues,
|
|
120
|
+
);
|
|
114
121
|
if (passthroughStyle !== undefined) {
|
|
115
122
|
reduced['style'] =
|
|
116
|
-
reduced['style'] === undefined
|
|
123
|
+
reduced['style'] === undefined
|
|
124
|
+
? passthroughStyle
|
|
125
|
+
: [reduced['style'], passthroughStyle];
|
|
117
126
|
}
|
|
118
127
|
reduced['style'] = [this.anchorStyle(), reduced['style']];
|
|
119
128
|
return reduced;
|
|
@@ -162,7 +171,8 @@ export abstract class AnimatedComponentBase
|
|
|
162
171
|
const hasPassthroughAnimatedValues =
|
|
163
172
|
this.passthroughAnimatedPropExplicitValues !== null &&
|
|
164
173
|
this.passthroughAnimatedPropExplicitValues !== undefined;
|
|
165
|
-
const wantsNative =
|
|
174
|
+
const wantsNative =
|
|
175
|
+
hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
|
|
166
176
|
this.leafBinder.reconcile(props, wantsNative);
|
|
167
177
|
}
|
|
168
178
|
|
|
@@ -171,7 +181,9 @@ export abstract class AnimatedComponentBase
|
|
|
171
181
|
// never disagree.
|
|
172
182
|
private mergedProps(): Record<string, unknown> {
|
|
173
183
|
const base = this.animatedProps ?? {};
|
|
174
|
-
return this.style === undefined
|
|
184
|
+
return this.style === undefined
|
|
185
|
+
? { ...base }
|
|
186
|
+
: { ...base, style: this.style };
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
// The committed host node held by IDENTITY. resolveHostNode unwraps an imperative scroll
|
|
@@ -186,7 +198,9 @@ export abstract class AnimatedComponentBase
|
|
|
186
198
|
@Component({
|
|
187
199
|
selector: 'AnimatedView, symbiote-animated-view',
|
|
188
200
|
standalone: true,
|
|
189
|
-
hostDirectives: [
|
|
201
|
+
hostDirectives: [
|
|
202
|
+
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
203
|
+
],
|
|
190
204
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
191
205
|
imports: [SymbioteHostPropsDirective, ViewHost],
|
|
192
206
|
inputs: ANIMATED_INPUTS,
|
|
@@ -202,7 +216,9 @@ export class AnimatedView extends AnimatedComponentBase {}
|
|
|
202
216
|
@Component({
|
|
203
217
|
selector: 'AnimatedText, symbiote-animated-text',
|
|
204
218
|
standalone: true,
|
|
205
|
-
hostDirectives: [
|
|
219
|
+
hostDirectives: [
|
|
220
|
+
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
221
|
+
],
|
|
206
222
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
207
223
|
imports: [SymbioteHostPropsDirective, TextHost],
|
|
208
224
|
inputs: ANIMATED_INPUTS,
|
|
@@ -218,7 +234,9 @@ export class AnimatedText extends AnimatedComponentBase {}
|
|
|
218
234
|
@Component({
|
|
219
235
|
selector: 'AnimatedImage, symbiote-animated-image',
|
|
220
236
|
standalone: true,
|
|
221
|
-
hostDirectives: [
|
|
237
|
+
hostDirectives: [
|
|
238
|
+
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
239
|
+
],
|
|
222
240
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
223
241
|
imports: [SymbioteHostPropsDirective, ImageHost],
|
|
224
242
|
inputs: ANIMATED_IMAGE_INPUTS,
|
|
@@ -242,7 +260,10 @@ export class AnimatedText extends AnimatedComponentBase {}
|
|
|
242
260
|
`,
|
|
243
261
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
244
262
|
})
|
|
245
|
-
export class AnimatedImage
|
|
263
|
+
export class AnimatedImage
|
|
264
|
+
extends ImageBase
|
|
265
|
+
implements AfterViewInit, OnChanges, OnDestroy
|
|
266
|
+
{
|
|
246
267
|
animatedProps: Record<string, unknown> | undefined;
|
|
247
268
|
passthroughAnimatedPropExplicitValues: unknown;
|
|
248
269
|
|
|
@@ -250,7 +271,8 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
|
|
|
250
271
|
// comment on AnimatedComponentBase.elementRef.
|
|
251
272
|
private readonly elementRef = inject(ElementRef);
|
|
252
273
|
|
|
253
|
-
@ViewChild(SymbioteHostPropsDirective)
|
|
274
|
+
@ViewChild(SymbioteHostPropsDirective)
|
|
275
|
+
private hostDirective?: SymbioteHostPropsDirective;
|
|
254
276
|
|
|
255
277
|
// Same Pure Fabrication AnimatedComponentBase holds — see its matching comment. AnimatedImage
|
|
256
278
|
// can't extend AnimatedComponentBase (it must extend ImageBase, see the file header), so it
|
|
@@ -263,10 +285,14 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
|
|
|
263
285
|
|
|
264
286
|
get animatedImageProps(): Record<string, unknown> {
|
|
265
287
|
const reduced = reduceProps(this.mergedProps());
|
|
266
|
-
const passthroughStyle = readPassthroughStyle(
|
|
288
|
+
const passthroughStyle = readPassthroughStyle(
|
|
289
|
+
this.passthroughAnimatedPropExplicitValues,
|
|
290
|
+
);
|
|
267
291
|
if (passthroughStyle !== undefined) {
|
|
268
292
|
reduced['style'] =
|
|
269
|
-
reduced['style'] === undefined
|
|
293
|
+
reduced['style'] === undefined
|
|
294
|
+
? passthroughStyle
|
|
295
|
+
: [reduced['style'], passthroughStyle];
|
|
270
296
|
}
|
|
271
297
|
const resolved = resolveImageProps(reduced);
|
|
272
298
|
resolved['style'] = [anchorHostStyle(this.elementRef), resolved['style']];
|
|
@@ -311,7 +337,8 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
|
|
|
311
337
|
const hasPassthroughAnimatedValues =
|
|
312
338
|
this.passthroughAnimatedPropExplicitValues !== null &&
|
|
313
339
|
this.passthroughAnimatedPropExplicitValues !== undefined;
|
|
314
|
-
const wantsNative =
|
|
340
|
+
const wantsNative =
|
|
341
|
+
hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
|
|
315
342
|
this.leafBinder.reconcile(props, wantsNative);
|
|
316
343
|
}
|
|
317
344
|
|
|
@@ -332,7 +359,9 @@ export class AnimatedImage extends ImageBase implements AfterViewInit, OnChanges
|
|
|
332
359
|
@Component({
|
|
333
360
|
selector: 'AnimatedScrollView, symbiote-animated-scroll-view',
|
|
334
361
|
standalone: true,
|
|
335
|
-
hostDirectives: [
|
|
362
|
+
hostDirectives: [
|
|
363
|
+
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
364
|
+
],
|
|
336
365
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
337
366
|
imports: [SymbioteHostPropsDirective, ScrollViewHost],
|
|
338
367
|
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 = (
|
|
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 {
|
|
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(
|
|
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 & {
|
|
57
|
-
|
|
64
|
+
export const StatusBar: typeof StatusBarWithStatics & {
|
|
65
|
+
readonly currentHeight?: number;
|
|
66
|
+
} = StatusBarWithStatics;
|
package/src/primitives/index.ts
CHANGED
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
// component imports them and renders them in its template. Declaring `style` as a real
|
|
4
4
|
// Angular input prevents Angular's CSS style engine from decomposing RN `StyleProp` arrays.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Component,
|
|
8
|
+
Input,
|
|
9
|
+
type OnInit,
|
|
10
|
+
type SimpleChanges,
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
import {
|
|
13
|
+
resolveTextProps,
|
|
14
|
+
type IEllipsizeMode,
|
|
15
|
+
} from '@symbiote-native/components';
|
|
7
16
|
import { SymbiotePrimitiveHost } from './shared';
|
|
8
17
|
|
|
9
18
|
export {
|
|
@@ -21,12 +30,47 @@ export {
|
|
|
21
30
|
})
|
|
22
31
|
export class ViewHost extends SymbiotePrimitiveHost {}
|
|
23
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Text carries the two defaults RN's Text.js applies unconditionally (`ellipsizeMode ?? 'tail'`,
|
|
35
|
+
* `allowFontScaling !== false`, Text.js:288-291). Without them native falls back to `clip` and a
|
|
36
|
+
* clamped Text cuts mid-word with no ellipsis — device-observed on the other adapters 2026-08-19.
|
|
37
|
+
*
|
|
38
|
+
* They are declared as real `@Input()`s, unlike the pass-through props the base's comment
|
|
39
|
+
* describes, for one reason: the default can only be applied by code that can SEE whether the
|
|
40
|
+
* caller supplied a value. A host-binding pass-through is invisible here, so blindly writing
|
|
41
|
+
* 'tail' would silently overwrite an explicit `ellipsizeMode="clip"`.
|
|
42
|
+
*/
|
|
24
43
|
@Component({
|
|
25
44
|
selector: 'symbiote-text, Text',
|
|
26
45
|
standalone: true,
|
|
27
46
|
template: '<ng-content></ng-content>',
|
|
28
47
|
})
|
|
29
|
-
export class TextHost extends SymbiotePrimitiveHost {
|
|
48
|
+
export class TextHost extends SymbiotePrimitiveHost implements OnInit {
|
|
49
|
+
@Input() ellipsizeMode?: IEllipsizeMode;
|
|
50
|
+
@Input() allowFontScaling?: boolean;
|
|
51
|
+
|
|
52
|
+
// ngOnChanges fires before ngOnInit when a binding exists, and not at all when none does —
|
|
53
|
+
// so both hooks are needed to guarantee the defaults land exactly once per settled value.
|
|
54
|
+
ngOnInit(): void {
|
|
55
|
+
this.applyTextDefaults();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override ngOnChanges(changes: SimpleChanges): void {
|
|
59
|
+
super.ngOnChanges(changes);
|
|
60
|
+
if ('ellipsizeMode' in changes || 'allowFontScaling' in changes) {
|
|
61
|
+
this.applyTextDefaults();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private applyTextDefaults(): void {
|
|
66
|
+
const resolved = resolveTextProps({
|
|
67
|
+
ellipsizeMode: this.ellipsizeMode,
|
|
68
|
+
allowFontScaling: this.allowFontScaling,
|
|
69
|
+
});
|
|
70
|
+
this.setHostProp('ellipsizeMode', resolved.ellipsizeMode);
|
|
71
|
+
this.setHostProp('allowFontScaling', resolved.allowFontScaling);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
30
74
|
|
|
31
75
|
@Component({
|
|
32
76
|
selector: 'symbiote-image',
|
package/src/primitives/shared.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Renderer2,
|
|
10
10
|
type SimpleChanges,
|
|
11
11
|
} from '@angular/core';
|
|
12
|
+
import { countAngular } from '../diagnostics';
|
|
12
13
|
import {
|
|
13
14
|
flattenStyle,
|
|
14
15
|
isSymbioteNode,
|
|
@@ -61,7 +62,9 @@ function isStyleValue<T>(value: unknown): value is IStyleProp<T> {
|
|
|
61
62
|
);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
export function anchorStyleProp<T>(
|
|
65
|
+
export function anchorStyleProp<T>(
|
|
66
|
+
elementRef: ElementRef<unknown>,
|
|
67
|
+
): IStyleProp<T> | undefined {
|
|
65
68
|
const value = anchorHostStyle(elementRef);
|
|
66
69
|
return isStyleValue<T>(value) ? value : undefined;
|
|
67
70
|
}
|
|
@@ -70,7 +73,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
70
73
|
return typeof value === 'object' && value !== null;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
function shallowStyleEqual(
|
|
76
|
+
function shallowStyleEqual(
|
|
77
|
+
a: Record<string, unknown>,
|
|
78
|
+
b: Record<string, unknown>,
|
|
79
|
+
): boolean {
|
|
74
80
|
const aKeys = Object.keys(a);
|
|
75
81
|
if (aKeys.length !== Object.keys(b).length) return false;
|
|
76
82
|
return aKeys.every(key => a[key] === b[key]);
|
|
@@ -97,7 +103,9 @@ export function stableAnchorStyle(
|
|
|
97
103
|
previous: unknown,
|
|
98
104
|
): Record<string, unknown> {
|
|
99
105
|
const next = flattenStyle([anchorHostStyle(elementRef), explicitStyle]);
|
|
100
|
-
return isRecord(previous) && shallowStyleEqual(previous, next)
|
|
106
|
+
return isRecord(previous) && shallowStyleEqual(previous, next)
|
|
107
|
+
? previous
|
|
108
|
+
: next;
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
const ON_PREFIX = /^on[A-Z]/;
|
|
@@ -166,9 +174,11 @@ export class SymbioteStyleInputDirective implements DoCheck, OnChanges {
|
|
|
166
174
|
// check re-dirties the view every tick and free-runs CD, the hazard `stableAnchorStyle` above
|
|
167
175
|
// prevents one level up.
|
|
168
176
|
ngDoCheck(): void {
|
|
177
|
+
countAngular('styleChecks');
|
|
169
178
|
const current = anchorHostStyle(this.elementRef);
|
|
170
179
|
if (isAnchorStyleUnchanged(this.lastAnchorStyle, current)) return;
|
|
171
180
|
this.lastAnchorStyle = current;
|
|
181
|
+
countAngular('styleMarks');
|
|
172
182
|
this.cdr.markForCheck();
|
|
173
183
|
}
|
|
174
184
|
}
|
|
@@ -178,7 +188,9 @@ export class SymbioteStyleInputDirective implements DoCheck, OnChanges {
|
|
|
178
188
|
// above into a CD loop.
|
|
179
189
|
function isAnchorStyleUnchanged(previous: unknown, next: unknown): boolean {
|
|
180
190
|
if (Object.is(previous, next)) return true;
|
|
181
|
-
return
|
|
191
|
+
return (
|
|
192
|
+
isRecord(previous) && isRecord(next) && shallowStyleEqual(previous, next)
|
|
193
|
+
);
|
|
182
194
|
}
|
|
183
195
|
|
|
184
196
|
/**
|
|
@@ -194,10 +206,17 @@ function isAnchorStyleUnchanged(previous: unknown, next: unknown): boolean {
|
|
|
194
206
|
* custom renderer's `setProperty`, so the engine's `routeProp` handles them as usual.
|
|
195
207
|
*/
|
|
196
208
|
@Directive()
|
|
197
|
-
export abstract class SymbiotePrimitiveHost implements OnChanges {
|
|
209
|
+
export abstract class SymbiotePrimitiveHost implements DoCheck, OnChanges {
|
|
198
210
|
private readonly renderer = inject(Renderer2);
|
|
199
211
|
private readonly elementRef = inject(ElementRef);
|
|
200
212
|
|
|
213
|
+
// Counting only. Angular calls this once per view per check, so it is the only honest measure of
|
|
214
|
+
// how much tree a tick walks - and with CheckAlways primitives that is the whole application, not
|
|
215
|
+
// the screen on top. One integer increment next to a template refresh is not measurable.
|
|
216
|
+
ngDoCheck(): void {
|
|
217
|
+
countAngular('viewsChecked');
|
|
218
|
+
}
|
|
219
|
+
|
|
201
220
|
@Input() style?: IStyleProp<unknown>;
|
|
202
221
|
|
|
203
222
|
/**
|
|
@@ -210,8 +229,18 @@ export abstract class SymbiotePrimitiveHost implements OnChanges {
|
|
|
210
229
|
|
|
211
230
|
ngOnChanges(changes: SimpleChanges): void {
|
|
212
231
|
if (!('style' in changes)) return;
|
|
213
|
-
const value =
|
|
214
|
-
|
|
232
|
+
const value =
|
|
233
|
+
this.style === undefined ? undefined : flattenStyle(this.style);
|
|
234
|
+
this.setHostProp('style', value);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Push one resolved prop onto the host element. `protected` rather than exposing the injected
|
|
239
|
+
* Renderer2/ElementRef, so a subclass applying its own defaults (TextHost) uses the same single
|
|
240
|
+
* write path this base already uses for `style`.
|
|
241
|
+
*/
|
|
242
|
+
protected setHostProp(key: string, value: unknown): void {
|
|
243
|
+
this.renderer.setProperty(this.elementRef.nativeElement, key, value);
|
|
215
244
|
}
|
|
216
245
|
}
|
|
217
246
|
|
|
@@ -246,16 +275,48 @@ export class SymbioteHostPropsDirective {
|
|
|
246
275
|
// Reference stability is the point, not just the allocation: a fresh function on every push
|
|
247
276
|
// defeats every downstream identity check on the bag, so nothing upstream could ever conclude
|
|
248
277
|
// "this bag is unchanged" while a callback prop was in it.
|
|
249
|
-
private readonly wrappers = new WeakMap<
|
|
278
|
+
private readonly wrappers = new WeakMap<
|
|
279
|
+
object,
|
|
280
|
+
(...args: unknown[]) => unknown
|
|
281
|
+
>();
|
|
282
|
+
|
|
283
|
+
// Values pushed by the previous bag, so a push writes only what actually moved. Copied rather
|
|
284
|
+
// than aliased: a component is free to hand back the same object with mutated fields.
|
|
285
|
+
private pushed: Record<string, unknown> = {};
|
|
250
286
|
|
|
251
287
|
get node(): unknown {
|
|
252
288
|
return this.elementRef.nativeElement;
|
|
253
289
|
}
|
|
254
290
|
|
|
291
|
+
// MEASURED 2026-08-20, headless 1000-row create: writing every key of every bag cost 104 000
|
|
292
|
+
// engine setProp calls where Solid's identical screen cost 12 000 — and 90 000 of them carried
|
|
293
|
+
// `undefined`. A composed component's bag is a FIXED-SHAPE literal (Pressable's is ~48 keys,
|
|
294
|
+
// most of them unset on any given instance), so the first push of a fresh node spends most of
|
|
295
|
+
// its work deleting keys the node never had. Diffing per key removes exactly that: an unset key
|
|
296
|
+
// is skipped on mount and still written the moment it changes to or from a real value.
|
|
255
297
|
set symbioteHostProps(props: Record<string, unknown>) {
|
|
298
|
+
const pushed = this.pushed;
|
|
299
|
+
// Only keys the node actually carries, so the vanished-key sweep below walks a handful rather
|
|
300
|
+
// than the bag's whole declared shape.
|
|
301
|
+
const next: Record<string, unknown> = {};
|
|
256
302
|
for (const [key, value] of Object.entries(props)) {
|
|
257
|
-
|
|
303
|
+
if (value !== undefined) next[key] = value;
|
|
304
|
+
if (Object.is(pushed[key], value)) continue;
|
|
305
|
+
this.renderer.setProperty(
|
|
306
|
+
this.elementRef.nativeElement,
|
|
307
|
+
key,
|
|
308
|
+
this.wrapCallback(key, value),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
// A bag's key SET is not fixed across pushes — resolveAccessibilityProps folds aria-* aliases
|
|
312
|
+
// into a different shape once any of them holds a value — and a key that vanishes must clear
|
|
313
|
+
// the prop rather than leave the last value on the native view. A key still IN the bag was
|
|
314
|
+
// already handled above, `undefined` included.
|
|
315
|
+
for (const key of Object.keys(pushed)) {
|
|
316
|
+
if (key in props) continue;
|
|
317
|
+
this.renderer.setProperty(this.elementRef.nativeElement, key, undefined);
|
|
258
318
|
}
|
|
319
|
+
this.pushed = next;
|
|
259
320
|
}
|
|
260
321
|
|
|
261
322
|
// A flat-bag `onX` callback (responder negotiation, onLongPress, …) is invoked by the engine's
|
package/src/render/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
createSurface,
|
|
11
11
|
disposeRoot,
|
|
12
12
|
dlog,
|
|
13
|
+
reportUncaughtError,
|
|
13
14
|
toPublicInstance,
|
|
14
15
|
type ISymbioteNode,
|
|
15
16
|
type IRootTag,
|
|
@@ -62,7 +63,35 @@ function asAngularHost(hostNode: SymbioteSurface | ISymbioteNode): Element {
|
|
|
62
63
|
// to the wrapper as projectable content.
|
|
63
64
|
function createDetachedViewHost(): ISymbioteNode {
|
|
64
65
|
const descriptor = descriptorFor('View');
|
|
65
|
-
return toPublicInstance(
|
|
66
|
+
return toPublicInstance(
|
|
67
|
+
createEngineElement(descriptor.component, descriptor.isText),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Angular's whole unhandled-error funnel: INTERNAL_APPLICATION_ERROR_HANDLER resolves this token
|
|
72
|
+
// and hands it everything the framework caught on the app's behalf — a scheduled (async) tick that
|
|
73
|
+
// threw, a template listener that threw, a rejected pending task. Routed to the engine so an
|
|
74
|
+
// Angular app reaches the same native redbox as a React one instead of a console line nobody sees
|
|
75
|
+
// off a dev machine.
|
|
76
|
+
//
|
|
77
|
+
// There is deliberately no caught-vs-uncaught split here, unlike the React adapter's
|
|
78
|
+
// onUncaughtError/onCaughtError pair. Angular has no error boundary, and every path where the app
|
|
79
|
+
// DID handle something keeps clear of this seam on its own: a `@defer` block with an `@error`
|
|
80
|
+
// branch renders that branch (defer/rendering.ts calls handleUncaughtError only when there is
|
|
81
|
+
// none), `resource()` parks the failure in its own error signal, and an app-level try/catch or
|
|
82
|
+
// `catchError` never reaches Angular at all. So anything arriving here is by construction
|
|
83
|
+
// unclaimed, and the redbox is the right answer for all of it.
|
|
84
|
+
//
|
|
85
|
+
// Two things this must not do. It must not also call `super.handleError` — reportUncaughtError
|
|
86
|
+
// picks exactly one channel (host reporter or console) precisely because RN routes console.error
|
|
87
|
+
// into LogBox too, so the pair would double-report every error. And it must not throw: the
|
|
88
|
+
// scheduler invokes this from inside its own `catch` (zoneless_scheduling_impl.ts's tick), where a
|
|
89
|
+
// second throw escapes to the host and kills the "report and keep running" behaviour the provider
|
|
90
|
+
// exists for.
|
|
91
|
+
class SymbioteErrorHandler extends ErrorHandler {
|
|
92
|
+
override handleError(error: unknown): void {
|
|
93
|
+
reportUncaughtError(error, { origin: 'angular render' });
|
|
94
|
+
}
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
interface IMountedApp {
|
|
@@ -81,7 +110,10 @@ export interface IMountOptions {
|
|
|
81
110
|
wrapperComponent?: Type<unknown>;
|
|
82
111
|
}
|
|
83
112
|
|
|
84
|
-
function applyInputs(
|
|
113
|
+
function applyInputs(
|
|
114
|
+
cmpRef: ComponentRef<unknown>,
|
|
115
|
+
initialProps: object | undefined,
|
|
116
|
+
): void {
|
|
85
117
|
if (initialProps === undefined) return;
|
|
86
118
|
for (const [key, value] of Object.entries(initialProps)) {
|
|
87
119
|
cmpRef.setInput(key, value);
|
|
@@ -118,7 +150,23 @@ export function mount(
|
|
|
118
150
|
provide: RendererFactory2,
|
|
119
151
|
useValue: new SymbioteRendererFactory(surface),
|
|
120
152
|
},
|
|
121
|
-
|
|
153
|
+
// `getElementById` is not decoration: `resource()` resolves TransferState, whose root
|
|
154
|
+
// factory runs `retrieveTransferredState(doc, appId)` ->
|
|
155
|
+
// `doc.getElementById(appId + '-state')` (core/src/transfer_state.ts:156) to pick up
|
|
156
|
+
// server-rendered state. A stub without it makes that a TypeError, and because the throw
|
|
157
|
+
// happens while the component is being constructed, the ENTIRE screen renders nothing —
|
|
158
|
+
// white body under a native header that navigation drew anyway, no error surfaced. The
|
|
159
|
+
// `optional: true` on the injector lookup does not help: the token resolves, its factory
|
|
160
|
+
// is what throws. Returning null is the honest client answer — there is no server here, so
|
|
161
|
+
// `script?.tagName` short-circuits and the caller gets `{}`.
|
|
162
|
+
{
|
|
163
|
+
provide: DOCUMENT,
|
|
164
|
+
useValue: {
|
|
165
|
+
head: surface,
|
|
166
|
+
body: surface,
|
|
167
|
+
getElementById: (): null => null,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
122
170
|
// createEnvironmentInjector with a null parent scopes this injector to {'environment'}
|
|
123
171
|
// only (see EnvironmentNgModuleRefAdapter), so providedIn:'root' tokens — ApplicationRef
|
|
124
172
|
// included — never resolve (R3Injector.get walks up for a `scopes` containing 'root', and
|
|
@@ -142,9 +190,11 @@ export function mount(
|
|
|
142
190
|
// `injector.get(ErrorHandler)`; a normal `bootstrapApplication` registers that token by
|
|
143
191
|
// default, but our from-scratch environment injector never did, so the lookup itself threw
|
|
144
192
|
// NG0201 and replaced the real error with "No provider found for ErrorHandler" — any async
|
|
145
|
-
// tick() exception crashed hard, uncaught, instead of being reported.
|
|
146
|
-
//
|
|
147
|
-
|
|
193
|
+
// tick() exception crashed hard, uncaught, instead of being reported. The token still has to
|
|
194
|
+
// be provided for exactly that reason; what it resolves to is now SymbioteErrorHandler
|
|
195
|
+
// (see above), which reports through the engine instead of Angular's `console.error('ERROR',
|
|
196
|
+
// e)` and, like the default, returns rather than rethrows so the app keeps running.
|
|
197
|
+
{ provide: ErrorHandler, useClass: SymbioteErrorHandler },
|
|
148
198
|
ColorSchemeService,
|
|
149
199
|
WindowDimensionsService,
|
|
150
200
|
],
|