@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.
- package/README.md +3 -3
- package/babel-lower-host-primitives.cjs +636 -0
- package/babel-register-composed.cjs +21 -3
- package/build/angular/anchor-host-registry.js +3 -1
- package/build/angular/anchor-host-registry.js.map +1 -1
- 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.d.ts +3 -0
- package/build/angular/components/button.js +22 -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 -10
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.js +4 -10
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.d.ts +4 -4
- package/build/angular/components/image/shared.js +51 -33
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.d.ts +3 -0
- package/build/angular/components/image-background.js +32 -9
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.d.ts +2 -1
- package/build/angular/components/input-accessory-view/index.js +18 -16
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
- package/build/angular/components/keyboard-avoiding-view/index.js +56 -19
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +1 -0
- package/build/angular/components/modal/index.js +21 -10
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +10 -5
- package/build/angular/components/pressable/index.js +61 -16
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.d.ts +2 -1
- package/build/angular/components/refresh-control/index.js +13 -9
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.d.ts +3 -1
- package/build/angular/components/safe-area-view/index.js +22 -20
- 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.d.ts +2 -0
- package/build/angular/components/scroll-view/shared.js +48 -11
- 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 +9 -1
- package/build/angular/components/section-list/index.js +72 -18
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.js +23 -9
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.js +23 -9
- 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 +8 -2
- package/build/angular/components/text-input.js +79 -34
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +36 -7
- package/build/angular/components/touchable/index.js +280 -72
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.d.ts +2 -1
- package/build/angular/components/touchable-native-feedback/index.js +19 -17
- 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 +8 -2
- package/build/angular/components/virtualized-list/index.js +207 -61
- 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 +21 -1
- package/build/angular/components/virtualized-section-list/index.js +100 -22
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +2 -2
- package/build/angular/components.js +1 -1
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.d.ts +11 -2
- package/build/angular/create-portal/index.js +42 -12
- 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/gate-demand.d.ts +37 -0
- package/build/angular/gate-demand.js +84 -0
- package/build/angular/gate-demand.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 +6 -2
- package/build/angular/index.js +16 -1
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.d.ts +1 -0
- package/build/angular/modules/animated/create-animated-component.js +41 -18
- 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 +31 -2
- package/build/angular/primitives/index.js +81 -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/register.d.ts +1 -0
- package/build/angular/register.js +35 -0
- package/build/angular/register.js.map +1 -0
- package/build/angular/render/index.js +51 -6
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +1 -0
- package/build/angular/renderer/index.js +157 -21
- 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/build/angular/state-style.d.ts +9 -0
- package/build/angular/state-style.js +39 -0
- package/build/angular/state-style.js.map +1 -0
- package/metro-transformer.cjs +69 -0
- package/package.json +27 -8
- package/src/anchor-host-registry.ts +3 -1
- 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 +36 -6
- package/src/components/flat-list/index.ts +91 -29
- package/src/components/image/index.android.ts +9 -6
- package/src/components/image/index.ios.ts +9 -6
- package/src/components/image/shared.ts +110 -45
- package/src/components/image-background.ts +47 -10
- package/src/components/input-accessory-view/index.ts +41 -18
- package/src/components/keyboard-avoiding-view/index.ts +76 -22
- package/src/components/modal/index.ts +39 -12
- package/src/components/pressable/index.ts +121 -31
- package/src/components/refresh-control/index.ts +36 -13
- package/src/components/safe-area-view/index.ts +44 -19
- 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 +124 -30
- package/src/components/scroll-view/sticky-header.ts +25 -8
- package/src/components/section-list/index.ts +79 -19
- package/src/components/switch/index.android.ts +18 -7
- package/src/components/switch/index.ios.ts +18 -7
- package/src/components/switch/shared.ts +15 -4
- package/src/components/text-input.ts +119 -31
- package/src/components/touchable/index.ts +339 -72
- package/src/components/touchable-native-feedback/index.ts +50 -18
- package/src/components/virtualized-list/directives.ts +19 -5
- package/src/components/virtualized-list/index.ts +292 -72
- package/src/components/virtualized-section-list/directives.ts +6 -3
- package/src/components/virtualized-section-list/index.ts +164 -34
- package/src/components.ts +6 -1
- package/src/create-portal/index.ts +47 -11
- 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/gate-demand.ts +109 -0
- package/src/host-instance/index.ts +9 -3
- package/src/index.ts +33 -2
- package/src/modules/animated/create-animated-component.ts +67 -19
- package/src/modules/app-registry/index.ts +3 -1
- package/src/modules/status-bar/index.ts +13 -4
- package/src/primitives/index.ts +73 -2
- package/src/primitives/shared.ts +70 -9
- package/src/register.ts +43 -0
- package/src/render/index.ts +59 -7
- package/src/renderer/index.ts +209 -37
- package/src/services/color-scheme.service.ts +3 -1
- package/src/services/window-dimensions.service.ts +11 -4
- package/src/state-style.ts +39 -0
package/src/renderer/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
removeChild,
|
|
19
19
|
routeProp,
|
|
20
20
|
setEventListener,
|
|
21
|
+
setProp,
|
|
21
22
|
setText,
|
|
22
23
|
toPublicInstance,
|
|
23
24
|
RAW_TEXT_COMPONENT,
|
|
@@ -25,9 +26,16 @@ import {
|
|
|
25
26
|
type ISymbioteNode,
|
|
26
27
|
} from '@symbiote-native/engine';
|
|
27
28
|
import { descriptorFor } from '@symbiote-native/components';
|
|
29
|
+
import { foldHostBag } from '@symbiote-native/components/fold-host-bag';
|
|
28
30
|
import type { Renderer2, RendererFactory2, RendererType2 } from '@angular/core';
|
|
29
31
|
import { isAnchorHostComponent } from '../anchor-host-registry';
|
|
30
32
|
import {
|
|
33
|
+
countAngular,
|
|
34
|
+
noteAngularCreate,
|
|
35
|
+
noteAngularWrite,
|
|
36
|
+
} from '../diagnostics';
|
|
37
|
+
import {
|
|
38
|
+
flushScrollViewProjections,
|
|
31
39
|
getScrollViewProjection,
|
|
32
40
|
removeScrollViewProjectedChild,
|
|
33
41
|
} from '../components/scroll-view/projection';
|
|
@@ -49,6 +57,64 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
49
57
|
return typeof value === 'object' && value !== null;
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
// RN's Text.js applies two defaults on the way to native (core/components/host-primitives.cjs's
|
|
61
|
+
// `Text.defaults`; the authority on what they MEAN is core/components/src/text-props.ts's
|
|
62
|
+
// resolveTextProps, which the composed `Text` @Component already calls). That component's own
|
|
63
|
+
// host paints directly (Text is not anchor-hosted — see the top-level "View/Text's own component
|
|
64
|
+
// doesn't have this split" reasoning elsewhere in this file), so createElement runs for its INNER
|
|
65
|
+
// symbiote-text node too; seeding here therefore covers both the composed Text and any bare
|
|
66
|
+
// `symbiote-text` a future lowering emits, uniformly. Found missing 2026-08-31 (a cross-adapter
|
|
67
|
+
// key-count diff against Vue's real BenchmarkRow.vue) — without this a lowered Text's
|
|
68
|
+
// `numberOfLines` clips with no ellipsis, silently, on device only. Vue's renderer already does
|
|
69
|
+
// this (`adapters/vue/src/renderer/index.ts`'s `seedTextDefaults`); Angular's simply never did.
|
|
70
|
+
//
|
|
71
|
+
// Sourced from `foldHostBag` (`@symbiote-native/components/fold-host-bag`, driven by
|
|
72
|
+
// `HOST_PRIMITIVES.Text.defaults`) rather than a second hardcoded copy — React and Svelte call the
|
|
73
|
+
// same function directly; this used to be a THIRD, independent restatement of the same two
|
|
74
|
+
// defaults, with nothing to catch it drifting from the spec if a default's value ever changed.
|
|
75
|
+
// `foldHostBag('symbiote-text', {})` on an EMPTY bag folds every default with no authored value to
|
|
76
|
+
// override it (the alias loop has nothing to fold — `id` is only rewritten when present), which is
|
|
77
|
+
// exactly the seed this function needs.
|
|
78
|
+
function seedTextDefaults(node: ISymbioteNode): void {
|
|
79
|
+
const seeded = foldHostBag('symbiote-text', {});
|
|
80
|
+
for (const [key, value] of Object.entries(seeded)) setProp(node, key, value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// An explicit `undefined` must NOT clear one of those defaults — RN treats a missing prop and an
|
|
84
|
+
// explicit undefined alike, and only a literal `false` opts allowFontScaling out. Reached only
|
|
85
|
+
// when a later write clears a key back to undefined, so it costs nothing on the common path.
|
|
86
|
+
// `foldHostBag` folds every Text default when called this way (not just `key`), because its
|
|
87
|
+
// contract is "fold a whole bag" — the extra key computed alongside `key` is simply unread here.
|
|
88
|
+
function textDefaultFor(el: IHostElement, key: string): unknown {
|
|
89
|
+
if (isSurface(el) || !el.isText) return undefined;
|
|
90
|
+
return foldHostBag('symbiote-text', { [key]: undefined })[key];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// RN's `id` is the modern W3C-named alias for `nativeID` (core/components/host-primitives.cjs's
|
|
94
|
+
// `ID_ALIAS`) — View.js/Text.js copy it over unconditionally, so the two name ONE native prop.
|
|
95
|
+
// React/Solid/Svelte fold it in a wrapper or transform; Angular had it nowhere, so `<View
|
|
96
|
+
// id="x">`/`[id]="x"` reached Fabric with an unknown `id` key and no `nativeID` — silently, on
|
|
97
|
+
// device only. Lives in the renderer (mirroring Vue's `PROP_ALIASES`) so it covers every path
|
|
98
|
+
// that can set a prop — `setAttribute`, `setProperty`, and (should a future lowering emit one) a
|
|
99
|
+
// hand-built call — not just the composed component's own `id` @Input.
|
|
100
|
+
// `symbioteStyle` is what the lowering transform emits in place of `[style]`: Angular routes a
|
|
101
|
+
// `style` binding to its own CSS styling engine, which cannot represent an RN StyleProp (an array
|
|
102
|
+
// throws inside change detection). Under any other name it is an ordinary property binding and
|
|
103
|
+
// arrives here.
|
|
104
|
+
// Angular's two-way sugar `[(value)]` compiles to a `(valueChange)` binding; the engine knows the
|
|
105
|
+
// same fold as the function prop `onValueChange`. See `listen()`.
|
|
106
|
+
const VALUE_CHANGE_EVENT = 'valueChange';
|
|
107
|
+
const VALUE_CHANGE_PROP = 'onValueChange';
|
|
108
|
+
|
|
109
|
+
const PROP_ALIASES: ReadonlyMap<string, string> = new Map([
|
|
110
|
+
['id', 'nativeID'],
|
|
111
|
+
['symbioteStyle', 'style'],
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
function aliasedPropName(name: string): string {
|
|
115
|
+
return PROP_ALIASES.get(name) ?? name;
|
|
116
|
+
}
|
|
117
|
+
|
|
52
118
|
// Diagnostic-only: tags each anchor with a sequential id so log lines can tell distinct
|
|
53
119
|
// anchors apart (they all otherwise print the same generic '#anchor' component name).
|
|
54
120
|
// Gated behind isDebug() so a production build pays zero cost (no WeakMap writes).
|
|
@@ -68,7 +134,9 @@ function describeHost(node: IHostElement | null | undefined): string {
|
|
|
68
134
|
if (node === null || node === undefined) return 'null';
|
|
69
135
|
if (isSurface(node)) return 'surface';
|
|
70
136
|
const anchorId = anchorDebugIds.get(node);
|
|
71
|
-
return anchorId !== undefined
|
|
137
|
+
return anchorId !== undefined
|
|
138
|
+
? `${node.component}#${anchorId}`
|
|
139
|
+
: node.component;
|
|
72
140
|
}
|
|
73
141
|
|
|
74
142
|
const PRIMITIVE_SELECTOR_ALIAS: Record<string, string> = {
|
|
@@ -84,9 +152,15 @@ const PRIMITIVE_SELECTOR_ALIAS: Record<string, string> = {
|
|
|
84
152
|
// Inserting a bare raw-text node anywhere but inside a <Text> is invalid in Fabric (a
|
|
85
153
|
// stray RCTRawText would paint). Angular's ɵɵtext only ever lands text inside a <Text>,
|
|
86
154
|
// but guard anyway for parity with the Vue adapter and to fail loudly on a bad template.
|
|
155
|
+
// `removeScrollViewProjectedChild` takes a remove callback and uses it only when the child turns
|
|
156
|
+
// out to be projected, which almost none are — so the engine's own `removeChild` is passed by
|
|
157
|
+
// reference rather than wrapped in an arrow. The wrapper was allocated on EVERY removed node:
|
|
158
|
+
// 10 000 closures on a Clear whose engine window is 0.1 ms.
|
|
87
159
|
function assertTextPlacement(child: ISymbioteNode, parent: IHostElement): void {
|
|
88
160
|
if (isRawText(child) && (isSurface(parent) || !parent.isText)) {
|
|
89
|
-
throw new Error(
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Text string "${String(child.props.text)}" must be rendered inside a <Text>`,
|
|
163
|
+
);
|
|
90
164
|
}
|
|
91
165
|
}
|
|
92
166
|
|
|
@@ -109,18 +183,37 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
109
183
|
// name for a native leaf. Public aliases are normalized to their engine primitive name
|
|
110
184
|
// before descriptor lookup. descriptorFor resolves it; an unknown `symbiote-*` is a typo,
|
|
111
185
|
// any other string flows through as a raw Fabric name (events/processors derived from its
|
|
112
|
-
// ViewConfig).
|
|
113
|
-
//
|
|
186
|
+
// ViewConfig). The imperative API (measure / setNativeProps / focus) is on the node's
|
|
187
|
+
// prototype, so toPublicInstance hands back the SAME identity the commit mirror keys on.
|
|
188
|
+
countAngular('nodesCreated');
|
|
189
|
+
noteAngularCreate(name);
|
|
114
190
|
const engineName = PRIMITIVE_SELECTOR_ALIAS[name] ?? name;
|
|
191
|
+
|
|
115
192
|
if (isAnchorHostComponent(engineName)) {
|
|
116
193
|
const anchor = tagAnchorForDebug(createAnchor());
|
|
117
|
-
|
|
194
|
+
if (isDebug()) {
|
|
195
|
+
dlog(
|
|
196
|
+
`angular createElement ${name} -> anchor host ${describeHost(anchor)}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
118
199
|
return anchor;
|
|
119
200
|
}
|
|
120
201
|
|
|
121
202
|
const descriptor = descriptorFor(engineName);
|
|
122
|
-
|
|
123
|
-
|
|
203
|
+
// The tag, not just the resolved Fabric name: the host-behavior registry is keyed by the
|
|
204
|
+
// INTRINSIC tag (host-behavior.ts's own comment), and a node only ever carries the resolved
|
|
205
|
+
// name afterward. Omitting this argument means attachHostBehavior looks up the WRONG key
|
|
206
|
+
// (e.g. 'RCTView') and any registered behavior silently never attaches — Vue's renderer
|
|
207
|
+
// already passes this correctly (`createElement(descriptor.component, descriptor.isText, type)`).
|
|
208
|
+
const node = createElement(
|
|
209
|
+
descriptor.component,
|
|
210
|
+
descriptor.isText,
|
|
211
|
+
engineName,
|
|
212
|
+
);
|
|
213
|
+
if (descriptor.isText) seedTextDefaults(node);
|
|
214
|
+
if (isDebug()) {
|
|
215
|
+
dlog(`angular createElement ${name} -> ${descriptor.component}`);
|
|
216
|
+
}
|
|
124
217
|
return toPublicInstance(node);
|
|
125
218
|
}
|
|
126
219
|
|
|
@@ -128,12 +221,16 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
128
221
|
// Angular structural directives (*ngIf / @if / @for) need anchor nodes to track
|
|
129
222
|
// position. A real retained node the commit walk SKIPS — no native view. Twin of the
|
|
130
223
|
// Vue createComment path.
|
|
224
|
+
countAngular('nodesCreated');
|
|
131
225
|
const anchor = tagAnchorForDebug(createAnchor());
|
|
132
|
-
|
|
226
|
+
if (isDebug()) {
|
|
227
|
+
dlog(`Angular renderer createComment -> ${describeHost(anchor)}`);
|
|
228
|
+
}
|
|
133
229
|
return anchor;
|
|
134
230
|
}
|
|
135
231
|
|
|
136
232
|
createText(value: string): IHostNode {
|
|
233
|
+
countAngular('nodesCreated');
|
|
137
234
|
return createRawText(value);
|
|
138
235
|
}
|
|
139
236
|
|
|
@@ -142,15 +239,22 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
142
239
|
// (see parentNode below) — mirrors Angular's own `if (parentRNode !== null)` guard in
|
|
143
240
|
// addLViewToLContainer: skip silently now, the later projection pass places it correctly.
|
|
144
241
|
if (parent === null) return;
|
|
242
|
+
countAngular('nodesInserted');
|
|
145
243
|
assertTextPlacement(newChild, parent);
|
|
146
244
|
if (isSurface(parent)) {
|
|
147
|
-
|
|
245
|
+
if (isDebug()) {
|
|
246
|
+
dlog(
|
|
247
|
+
`Angular renderer appendChild parent=surface child=${describeHost(newChild)}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
148
250
|
parent.appendChild(newChild);
|
|
149
251
|
} else {
|
|
150
252
|
const projection = getScrollViewProjection(parent);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
253
|
+
if (isDebug()) {
|
|
254
|
+
dlog(
|
|
255
|
+
`Angular renderer appendChild parent=${describeHost(parent)} child=${describeHost(newChild)} projection=${projection !== undefined}`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
154
258
|
if (projection !== undefined) {
|
|
155
259
|
projection.appendProjectedChild(parent, newChild, (target, child) =>
|
|
156
260
|
appendChild(target, child),
|
|
@@ -162,25 +266,39 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
162
266
|
this.surface.requestCommit();
|
|
163
267
|
}
|
|
164
268
|
|
|
165
|
-
insertBefore(
|
|
269
|
+
insertBefore(
|
|
270
|
+
parent: IHostElement | null,
|
|
271
|
+
newChild: IHostNode,
|
|
272
|
+
refChild: IHostNode | null,
|
|
273
|
+
): void {
|
|
166
274
|
if (parent === null) return; // see the appendChild guard above
|
|
275
|
+
countAngular('nodesInserted');
|
|
167
276
|
assertTextPlacement(newChild, parent);
|
|
168
277
|
if (isSurface(parent)) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
278
|
+
if (isDebug()) {
|
|
279
|
+
dlog(
|
|
280
|
+
`Angular renderer insertBefore parent=surface child=${describeHost(newChild)} ref=${refChild ? describeHost(refChild) : 'null'}`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
172
283
|
if (refChild) parent.insertBefore(newChild, refChild);
|
|
173
284
|
else parent.appendChild(newChild);
|
|
174
285
|
} else {
|
|
175
286
|
const projection = getScrollViewProjection(parent);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
287
|
+
if (isDebug()) {
|
|
288
|
+
dlog(
|
|
289
|
+
`Angular renderer insertBefore parent=${describeHost(parent)} child=${describeHost(newChild)} ref=${refChild ? describeHost(refChild) : 'null'} projection=${projection !== undefined}`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
179
292
|
if (projection !== undefined) {
|
|
180
|
-
projection.insertProjectedChild(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
293
|
+
projection.insertProjectedChild(
|
|
294
|
+
parent,
|
|
295
|
+
newChild,
|
|
296
|
+
refChild,
|
|
297
|
+
(target, child, before) => {
|
|
298
|
+
if (before === undefined) appendChild(target, child);
|
|
299
|
+
else insertBefore(target, child, before);
|
|
300
|
+
},
|
|
301
|
+
);
|
|
184
302
|
} else if (refChild) {
|
|
185
303
|
insertBefore(parent, newChild, refChild);
|
|
186
304
|
} else {
|
|
@@ -194,14 +312,16 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
194
312
|
// Detach from the child's own retained parent (a top-level node lives in
|
|
195
313
|
// surface.children with no parent). Angular's `parent` arg is ignored in favor of the
|
|
196
314
|
// authoritative link, mirroring the Vue adapter's remove.
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
315
|
+
countAngular('nodesRemoved');
|
|
316
|
+
const wasProjected = removeScrollViewProjectedChild(oldChild, removeChild);
|
|
317
|
+
if (isDebug()) {
|
|
318
|
+
const angularParent = _parent !== null ? describeHost(_parent) : 'null';
|
|
319
|
+
const retainedParent =
|
|
320
|
+
oldChild.parent !== undefined ? describeHost(oldChild.parent) : 'none';
|
|
321
|
+
dlog(
|
|
322
|
+
`Angular renderer removeChild angularParent=${angularParent} retainedParent=${retainedParent} child=${describeHost(oldChild)} viaProjection=${wasProjected}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
205
325
|
if (!wasProjected) {
|
|
206
326
|
const parent = oldChild.parent;
|
|
207
327
|
if (parent !== undefined) removeChild(parent, oldChild);
|
|
@@ -228,7 +348,8 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
228
348
|
}
|
|
229
349
|
|
|
230
350
|
nextSibling(node: IHostNode): IHostNode | null {
|
|
231
|
-
const siblings =
|
|
351
|
+
const siblings =
|
|
352
|
+
node.parent !== undefined ? node.parent.children : this.surface.children;
|
|
232
353
|
const index = siblings.indexOf(node);
|
|
233
354
|
return index >= 0 ? (siblings[index + 1] ?? null) : null;
|
|
234
355
|
}
|
|
@@ -243,13 +364,18 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
243
364
|
|
|
244
365
|
setAttribute(el: IHostElement, name: string, value: string): void {
|
|
245
366
|
if (isSurface(el)) return;
|
|
246
|
-
|
|
367
|
+
countAngular('rendererWrites');
|
|
368
|
+
noteAngularWrite(name);
|
|
369
|
+
routeProp(el, aliasedPropName(name), value);
|
|
247
370
|
this.surface.requestCommit();
|
|
248
371
|
}
|
|
249
372
|
|
|
250
373
|
removeAttribute(el: IHostElement, name: string): void {
|
|
251
374
|
if (isSurface(el)) return;
|
|
252
|
-
|
|
375
|
+
countAngular('rendererWrites');
|
|
376
|
+
noteAngularWrite(name);
|
|
377
|
+
const aliased = aliasedPropName(name);
|
|
378
|
+
routeProp(el, aliased, textDefaultFor(el, aliased));
|
|
253
379
|
this.surface.requestCommit();
|
|
254
380
|
}
|
|
255
381
|
|
|
@@ -264,6 +390,8 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
264
390
|
|
|
265
391
|
addClass(el: IHostElement, name: string): void {
|
|
266
392
|
if (isSurface(el)) return;
|
|
393
|
+
countAngular('rendererWrites');
|
|
394
|
+
noteAngularWrite('class');
|
|
267
395
|
const tokens = this.classTokens.get(el) ?? new Set<string>();
|
|
268
396
|
tokens.add(name);
|
|
269
397
|
this.classTokens.set(el, tokens);
|
|
@@ -275,6 +403,8 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
275
403
|
if (isSurface(el)) return;
|
|
276
404
|
const tokens = this.classTokens.get(el);
|
|
277
405
|
if (tokens === undefined) return;
|
|
406
|
+
countAngular('rendererWrites');
|
|
407
|
+
noteAngularWrite('class');
|
|
278
408
|
tokens.delete(name);
|
|
279
409
|
routeProp(el, 'class', tokens.size > 0 ? [...tokens].join(' ') : undefined);
|
|
280
410
|
this.surface.requestCommit();
|
|
@@ -287,6 +417,8 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
287
417
|
// merge writes, and spreading an array as a record would silently produce numeric-index keys.
|
|
288
418
|
setStyle(el: IHostElement, style: string, value: unknown): void {
|
|
289
419
|
if (isSurface(el)) return;
|
|
420
|
+
countAngular('rendererWrites');
|
|
421
|
+
noteAngularWrite(`style.${style}`);
|
|
290
422
|
const current = getExplicitStyle(el);
|
|
291
423
|
const base = isRecord(current) ? current : {};
|
|
292
424
|
routeProp(el, 'style', { ...base, [style]: value });
|
|
@@ -297,6 +429,8 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
297
429
|
if (isSurface(el)) return;
|
|
298
430
|
const current = getExplicitStyle(el);
|
|
299
431
|
if (!isRecord(current)) return;
|
|
432
|
+
countAngular('rendererWrites');
|
|
433
|
+
noteAngularWrite(`style.${style}`);
|
|
300
434
|
const { [style]: _removed, ...rest } = current;
|
|
301
435
|
routeProp(el, 'style', rest);
|
|
302
436
|
this.surface.requestCommit();
|
|
@@ -306,14 +440,25 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
306
440
|
// ViewConfig (identical to React/Vue), so the whole flat-bag prop layer is shared.
|
|
307
441
|
setProperty(el: IHostElement, name: string, value: unknown): void {
|
|
308
442
|
if (isSurface(el)) return;
|
|
309
|
-
|
|
443
|
+
countAngular('rendererWrites');
|
|
444
|
+
noteAngularWrite(name);
|
|
445
|
+
const aliased = aliasedPropName(name);
|
|
446
|
+
routeProp(
|
|
447
|
+
el,
|
|
448
|
+
aliased,
|
|
449
|
+
value === undefined ? textDefaultFor(el, aliased) : value,
|
|
450
|
+
);
|
|
310
451
|
this.surface.requestCommit();
|
|
311
452
|
}
|
|
312
453
|
|
|
313
454
|
setValue(node: IHostNode, value: string): void {
|
|
455
|
+
countAngular('rendererWrites');
|
|
456
|
+
noteAngularWrite('#text');
|
|
314
457
|
// A useful permanent seam: text mutations are low-frequency and the one place a stale
|
|
315
458
|
// `{{binding}}` (a change-detection gap) shows up as "the setValue never fired".
|
|
316
|
-
|
|
459
|
+
if (isDebug()) {
|
|
460
|
+
dlog(`Angular renderer setValue "${value}" on ${describeHost(node)}`);
|
|
461
|
+
}
|
|
317
462
|
setText(node, value);
|
|
318
463
|
this.surface.requestCommit();
|
|
319
464
|
}
|
|
@@ -328,6 +473,20 @@ export class SymbioteRenderer implements Renderer2 {
|
|
|
328
473
|
callback: (event: unknown) => boolean | void,
|
|
329
474
|
): () => void {
|
|
330
475
|
if (!isSymbioteNode(target)) return () => {};
|
|
476
|
+
// `[(value)]` desugars to `(valueChange)`, which is the spelling every Angular template writes
|
|
477
|
+
// for a Switch or a TextInput. On the COMPONENT path it is an `@Output()` the wrapper derives
|
|
478
|
+
// from the raw `change` payload; on a LOWERED element there is no component, and registering
|
|
479
|
+
// `valueChange` as an engine event would wait forever for a Fabric event of that name.
|
|
480
|
+
//
|
|
481
|
+
// The lowered path already carries the same fold under RN's own spelling: both behaviors call
|
|
482
|
+
// `node.props.onValueChange(value, event)` — a plain function PROP, not an event
|
|
483
|
+
// (`behaviors/switch.ts`, `behaviors/text-input.ts`). So this is a rename, not a mechanism:
|
|
484
|
+
// route the binding to that prop and `[(value)]` behaves identically on both paths. Without it
|
|
485
|
+
// the transform had to refuse to lower the two primitives whose idiomatic spelling this is.
|
|
486
|
+
if (eventName === VALUE_CHANGE_EVENT) {
|
|
487
|
+
routeProp(target, VALUE_CHANGE_PROP, callback);
|
|
488
|
+
return () => routeProp(target, VALUE_CHANGE_PROP, undefined);
|
|
489
|
+
}
|
|
331
490
|
setEventListener(target, eventName, callback);
|
|
332
491
|
return () => setEventListener(target, eventName, undefined);
|
|
333
492
|
}
|
|
@@ -341,7 +500,20 @@ export class SymbioteRendererFactory implements RendererFactory2 {
|
|
|
341
500
|
|
|
342
501
|
constructor(private readonly surface: SymbioteSurface) {}
|
|
343
502
|
|
|
344
|
-
createRenderer(
|
|
503
|
+
createRenderer(
|
|
504
|
+
_hostElement: unknown,
|
|
505
|
+
_type: RendererType2 | null,
|
|
506
|
+
): Renderer2 {
|
|
345
507
|
return (this.renderer ??= new SymbioteRenderer(this.surface));
|
|
346
508
|
}
|
|
509
|
+
|
|
510
|
+
// Not commit coalescing (requestCommit owns that) — this is the one moment where a ScrollView's
|
|
511
|
+
// two input channels are known to be consistent: Angular has finished writing every `@Input` AND
|
|
512
|
+
// the renderer has finished every projected insert/remove for this pass, while the surface's
|
|
513
|
+
// commit is still only queued. Sticky projection reconciles are batched to here for both reasons;
|
|
514
|
+
// see flushScrollViewProjections.
|
|
515
|
+
end(): void {
|
|
516
|
+
countAngular('cdPasses');
|
|
517
|
+
flushScrollViewProjections();
|
|
518
|
+
}
|
|
347
519
|
}
|
|
@@ -11,7 +11,9 @@ import { Appearance, type IColorSchemeName } from '@symbiote-native/engine';
|
|
|
11
11
|
*/
|
|
12
12
|
@Injectable({ providedIn: 'root' })
|
|
13
13
|
export class ColorSchemeService {
|
|
14
|
-
readonly colorScheme = signal<IColorSchemeName | null>(
|
|
14
|
+
readonly colorScheme = signal<IColorSchemeName | null>(
|
|
15
|
+
Appearance.getColorScheme(),
|
|
16
|
+
);
|
|
15
17
|
|
|
16
18
|
constructor() {
|
|
17
19
|
const destroyRef = inject(DestroyRef);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { DestroyRef, inject, Injectable, signal } from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Dimensions,
|
|
4
|
+
type IDimensionsSet,
|
|
5
|
+
type IDisplayMetrics,
|
|
6
|
+
} from '@symbiote-native/engine';
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Angular twin of React's `useWindowDimensions` and Vue's `useWindowDimensions` composable. The
|
|
@@ -28,9 +32,12 @@ export class WindowDimensionsService {
|
|
|
28
32
|
}
|
|
29
33
|
};
|
|
30
34
|
|
|
31
|
-
const subscription = Dimensions.addEventListener(
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const subscription = Dimensions.addEventListener(
|
|
36
|
+
'change',
|
|
37
|
+
(set: IDimensionsSet) => {
|
|
38
|
+
handleChange(set.window);
|
|
39
|
+
},
|
|
40
|
+
);
|
|
34
41
|
// Re-check once after subscribing to close the gap between construction and the listener.
|
|
35
42
|
handleChange(Dimensions.get('window'));
|
|
36
43
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Re-export only. The implementation is shared (`@symbiote-native/components`) because two adapters
|
|
2
|
+
// had written byte-identical copies; this file exists so the code a transform EMITS keeps importing
|
|
3
|
+
// from the adapter package the app already depends on, rather than reaching past it.
|
|
4
|
+
//
|
|
5
|
+
// Imported from the SUBPATH, not the barrel: every adapter barrel re-exports
|
|
6
|
+
// `@symbiote-native/components` wholesale, so a name on that barrel silently becomes public API on
|
|
7
|
+
// all five — which is how a transform internal nearly shipped as a supported export.
|
|
8
|
+
//
|
|
9
|
+
// Present on every adapter, including those with no lowering transform yet: a per-framework subpath
|
|
10
|
+
// that four of five declare is exactly the silent gap `tests/package-subpath-parity.test.ts` exists
|
|
11
|
+
// to catch, and the file costs a re-export. Deliberately NOT on the public barrel — the emitted
|
|
12
|
+
// code names it, app code does not.
|
|
13
|
+
export {
|
|
14
|
+
resolveStateStyle,
|
|
15
|
+
type IPressStateArgument,
|
|
16
|
+
type IResolvedStateStyle,
|
|
17
|
+
} from '@symbiote-native/components/state-style';
|
|
18
|
+
|
|
19
|
+
// Angular-only addition to this otherwise byte-identical file: a template expression can call a
|
|
20
|
+
// PIPE, never an arbitrary imported function — `resolveStateStyle(x)` in a template resolves as
|
|
21
|
+
// `ctx.resolveStateStyle(x)`, a component-instance method lookup, which the lowering transform has
|
|
22
|
+
// no component to add one to (verified against the installed compiler-cli, 2026-09-01: the linker
|
|
23
|
+
// compiles it straight to a `ctx.` property read with no error, so this fails at RUNTIME —
|
|
24
|
+
// `ctx.resolveStateStyle is not a function` — not at compile time). A pipe is the one thing that
|
|
25
|
+
// resolves through `dependencies` the way a component/directive does.
|
|
26
|
+
//
|
|
27
|
+
// `emitStyleExpressionOnce` still applies: the transform's `@let pair = (expr | resolveStateStyle);`
|
|
28
|
+
// evaluates `expr` exactly once (verified via the compiled `ɵɵpipeBind1` call) and `pair.style` /
|
|
29
|
+
// `pair.activeStyle` are two cheap reads of the local, never a second evaluation of `expr`.
|
|
30
|
+
import { Pipe, type PipeTransform } from '@angular/core';
|
|
31
|
+
import { resolveStateStyle as resolveStateStyleImpl } from '@symbiote-native/components/state-style';
|
|
32
|
+
import type { IResolvedStateStyle } from '@symbiote-native/components/state-style';
|
|
33
|
+
|
|
34
|
+
@Pipe({ name: 'resolveStateStyle', standalone: true })
|
|
35
|
+
export class SymbioteStateStylePipe implements PipeTransform {
|
|
36
|
+
transform(value: unknown): IResolvedStateStyle {
|
|
37
|
+
return resolveStateStyleImpl(value);
|
|
38
|
+
}
|
|
39
|
+
}
|