@symbiote-native/angular 3.1.3 → 3.2.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 (121) hide show
  1. package/build/angular/accessibility-events.d.ts +12 -0
  2. package/build/angular/accessibility-events.js +28 -0
  3. package/build/angular/accessibility-events.js.map +1 -0
  4. package/build/angular/accessibility-inputs.d.ts +40 -0
  5. package/build/angular/accessibility-inputs.js +130 -0
  6. package/build/angular/accessibility-inputs.js.map +1 -0
  7. package/build/angular/bootstrap.d.ts +3 -1
  8. package/build/angular/bootstrap.js +4 -1
  9. package/build/angular/bootstrap.js.map +1 -1
  10. package/build/angular/components/flat-list/flat-list-template.d.ts +1 -0
  11. package/build/angular/components/flat-list/flat-list-template.js +158 -0
  12. package/build/angular/components/flat-list/flat-list-template.js.map +1 -0
  13. package/build/angular/components/flat-list/index.d.ts +6 -61
  14. package/build/angular/components/flat-list/index.js +26 -463
  15. package/build/angular/components/flat-list/index.js.map +1 -1
  16. package/build/angular/components/keyboard-avoiding-view/index.d.ts +4 -40
  17. package/build/angular/components/keyboard-avoiding-view/index.js +7 -131
  18. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  19. package/build/angular/components/modal/index.d.ts +5 -41
  20. package/build/angular/components/modal/index.js +8 -133
  21. package/build/angular/components/modal/index.js.map +1 -1
  22. package/build/angular/components/section-list/index.d.ts +4 -51
  23. package/build/angular/components/section-list/index.js +18 -335
  24. package/build/angular/components/section-list/index.js.map +1 -1
  25. package/build/angular/components/virtualized-list/cell-registry.d.ts +35 -0
  26. package/build/angular/components/virtualized-list/cell-registry.js +133 -0
  27. package/build/angular/components/virtualized-list/cell-registry.js.map +1 -0
  28. package/build/angular/components/virtualized-list/index.d.ts +18 -253
  29. package/build/angular/components/virtualized-list/index.js +112 -1178
  30. package/build/angular/components/virtualized-list/index.js.map +1 -1
  31. package/build/angular/components/virtualized-list/list-a11y-forward.d.ts +1 -0
  32. package/build/angular/components/virtualized-list/list-a11y-forward.js +42 -0
  33. package/build/angular/components/virtualized-list/list-a11y-forward.js.map +1 -0
  34. package/build/angular/components/virtualized-list/list-bags.d.ts +32 -0
  35. package/build/angular/components/virtualized-list/list-bags.js +120 -0
  36. package/build/angular/components/virtualized-list/list-bags.js.map +1 -0
  37. package/build/angular/components/virtualized-list/list-effects.d.ts +31 -0
  38. package/build/angular/components/virtualized-list/list-effects.js +83 -0
  39. package/build/angular/components/virtualized-list/list-effects.js.map +1 -0
  40. package/build/angular/components/virtualized-list/list-events.d.ts +17 -0
  41. package/build/angular/components/virtualized-list/list-events.js +34 -0
  42. package/build/angular/components/virtualized-list/list-events.js.map +1 -0
  43. package/build/angular/components/virtualized-list/list-handle.d.ts +35 -0
  44. package/build/angular/components/virtualized-list/list-handle.js +61 -0
  45. package/build/angular/components/virtualized-list/list-handle.js.map +1 -0
  46. package/build/angular/components/virtualized-list/list-inputs.d.ts +78 -0
  47. package/build/angular/components/virtualized-list/list-inputs.js +150 -0
  48. package/build/angular/components/virtualized-list/list-inputs.js.map +1 -0
  49. package/build/angular/components/virtualized-list/list-props.d.ts +58 -0
  50. package/build/angular/components/virtualized-list/list-props.js +3 -0
  51. package/build/angular/components/virtualized-list/list-props.js.map +1 -0
  52. package/build/angular/components/virtualized-list/list-styles.d.ts +23 -0
  53. package/build/angular/components/virtualized-list/list-styles.js +26 -0
  54. package/build/angular/components/virtualized-list/list-styles.js.map +1 -0
  55. package/build/angular/components/virtualized-list/list-template.d.ts +1 -0
  56. package/build/angular/components/virtualized-list/list-template.js +176 -0
  57. package/build/angular/components/virtualized-list/list-template.js.map +1 -0
  58. package/build/angular/components/virtualized-section-list/index.d.ts +13 -70
  59. package/build/angular/components/virtualized-section-list/index.js +22 -455
  60. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  61. package/build/angular/components/virtualized-section-list/section-list-template.d.ts +1 -0
  62. package/build/angular/components/virtualized-section-list/section-list-template.js +107 -0
  63. package/build/angular/components/virtualized-section-list/section-list-template.js.map +1 -0
  64. package/build/angular/finalization-registry.d.ts +4 -0
  65. package/build/angular/finalization-registry.js +17 -0
  66. package/build/angular/finalization-registry.js.map +1 -0
  67. package/build/angular/index.d.ts +5 -1
  68. package/build/angular/index.js +5 -1
  69. package/build/angular/index.js.map +1 -1
  70. package/build/angular/native-view-base.d.ts +19 -0
  71. package/build/angular/native-view-base.js +48 -0
  72. package/build/angular/native-view-base.js.map +1 -0
  73. package/build/angular/render/index.d.ts +2 -1
  74. package/build/angular/render/index.js +46 -73
  75. package/build/angular/render/index.js.map +1 -1
  76. package/build/angular/services/connect-watched-signal.d.ts +2 -0
  77. package/build/angular/services/connect-watched-signal.js +9 -0
  78. package/build/angular/services/connect-watched-signal.js.map +1 -0
  79. package/build/angular/services/create-event-value-hook.d.ts +3 -0
  80. package/build/angular/services/create-event-value-hook.js +17 -0
  81. package/build/angular/services/create-event-value-hook.js.map +1 -0
  82. package/build/angular/services/create-resource-hook.d.ts +7 -0
  83. package/build/angular/services/create-resource-hook.js +20 -0
  84. package/build/angular/services/create-resource-hook.js.map +1 -0
  85. package/build/angular/services/index.d.ts +4 -0
  86. package/build/angular/services/index.js +4 -0
  87. package/build/angular/services/index.js.map +1 -1
  88. package/build/angular/services/permissions-service-base.d.ts +14 -0
  89. package/build/angular/services/permissions-service-base.js +38 -0
  90. package/build/angular/services/permissions-service-base.js.map +1 -0
  91. package/package.json +6 -6
  92. package/src/accessibility-events.ts +13 -0
  93. package/src/accessibility-inputs.ts +89 -0
  94. package/src/bootstrap.ts +6 -2
  95. package/src/components/flat-list/flat-list-template.ts +159 -0
  96. package/src/components/flat-list/index.ts +30 -307
  97. package/src/components/keyboard-avoiding-view/index.ts +4 -78
  98. package/src/components/modal/index.ts +7 -80
  99. package/src/components/section-list/index.ts +21 -172
  100. package/src/components/virtualized-list/cell-registry.ts +199 -0
  101. package/src/components/virtualized-list/index.ts +145 -1181
  102. package/src/components/virtualized-list/list-a11y-forward.ts +42 -0
  103. package/src/components/virtualized-list/list-bags.ts +142 -0
  104. package/src/components/virtualized-list/list-effects.ts +116 -0
  105. package/src/components/virtualized-list/list-events.ts +31 -0
  106. package/src/components/virtualized-list/list-handle.ts +92 -0
  107. package/src/components/virtualized-list/list-inputs.ts +149 -0
  108. package/src/components/virtualized-list/list-props.ts +81 -0
  109. package/src/components/virtualized-list/list-styles.ts +62 -0
  110. package/src/components/virtualized-list/list-template.ts +175 -0
  111. package/src/components/virtualized-section-list/index.ts +40 -351
  112. package/src/components/virtualized-section-list/section-list-template.ts +108 -0
  113. package/src/finalization-registry.ts +18 -0
  114. package/src/index.ts +12 -1
  115. package/src/native-view-base.ts +51 -0
  116. package/src/render/index.ts +76 -77
  117. package/src/services/connect-watched-signal.ts +15 -0
  118. package/src/services/create-event-value-hook.ts +24 -0
  119. package/src/services/create-resource-hook.ts +32 -0
  120. package/src/services/index.ts +4 -0
  121. package/src/services/permissions-service-base.ts +46 -0
@@ -0,0 +1,108 @@
1
+ // One synthesized `vListItem` template switches on the flattened entry kind and stamps the matching
2
+ // app template, since the inner list renders every cell through a single one
3
+
4
+ import { LIST_ACCESSIBILITY_FORWARD } from '../virtualized-list/list-a11y-forward';
5
+
6
+ export const VIRTUALIZED_SECTION_LIST_TEMPLATE = `
7
+ <VirtualizedList
8
+ [data]="flatEntries"
9
+ [getItem]="getEntry"
10
+ [getItemCount]="getEntryCount"
11
+ [keyExtractor]="entryKeyExtractor"
12
+ [getItemLayout]="entryItemLayout"
13
+ [stickyHeaderIndices]="stickyHeaderIndices"
14
+ [extraData]="extraData"
15
+ [inverted]="inverted"
16
+ [refreshing]="refreshing"
17
+ [progressViewOffset]="progressViewOffset"
18
+ [refreshRequested]="refreshRequested ?? refresh.observed"
19
+ (refresh)="resolvedOnRefresh?.()"
20
+ (endReached)="resolvedOnEndReached?.($event)"
21
+ [onEndReachedThreshold]="onEndReachedThreshold"
22
+ (startReached)="resolvedOnStartReached?.($event)"
23
+ [onStartReachedThreshold]="onStartReachedThreshold"
24
+ [initialNumToRender]="initialNumToRender"
25
+ [initialScrollIndex]="initialScrollIndex"
26
+ [maxToRenderPerBatch]="maxToRenderPerBatch"
27
+ [updateCellsBatchingPeriod]="updateCellsBatchingPeriod"
28
+ [windowSize]="windowSize"
29
+ [maintainVisibleContentPosition]="maintainVisibleContentPosition"
30
+ [onScroll]="onScroll"
31
+ [onScrollBeginDrag]="onScrollBeginDrag"
32
+ [onScrollEndDrag]="onScrollEndDrag"
33
+ [onMomentumScrollBegin]="onMomentumScrollBegin"
34
+ [onMomentumScrollEnd]="onMomentumScrollEnd"
35
+ [scrollEventThrottle]="scrollEventThrottle"
36
+ [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
37
+ [keyboardDismissMode]="keyboardDismissMode"
38
+ [removeClippedSubviews]="removeClippedSubviews"
39
+ [nestedScrollEnabled]="nestedScrollEnabled"
40
+ [style]="resolvedStyle"
41
+ [contentContainerStyle]="contentContainerStyle"
42
+ [testID]="testID"
43
+ [nativeID]="nativeID"
44
+ ${LIST_ACCESSIBILITY_FORWARD}
45
+ >
46
+ <!-- The single synthesized cell template: dispatch per flattened entry tag -->
47
+ <ng-template vListItem let-entry let-separators="separators">
48
+ @switch (entryKind(entry)) {
49
+ @case ('header') {
50
+ <ng-container
51
+ [vListOutlet]="sectionHeaderDir?.templateRef"
52
+ [vListOutletContext]="sectionContextOf(entry)"
53
+ ></ng-container>
54
+ }
55
+ @case ('footer') {
56
+ <ng-container
57
+ [vListOutlet]="sectionFooterDir?.templateRef"
58
+ [vListOutletContext]="sectionContextOf(entry)"
59
+ ></ng-container>
60
+ }
61
+ @case ('section-separator') {
62
+ <ng-container
63
+ [vListOutlet]="sectionSeparatorDir?.templateRef"
64
+ ></ng-container>
65
+ }
66
+ @case ('item') {
67
+ <ng-container
68
+ [vListOutlet]="sectionItemDir?.templateRef"
69
+ [vListOutletContext]="itemContextOf(entry, separators)"
70
+ ></ng-container>
71
+ }
72
+ }
73
+ </ng-template>
74
+
75
+ <!-- List-level slots: forward the app's directives to the inner list -->
76
+ @if (listHeaderDir !== undefined) {
77
+ <ng-template vListHeader>
78
+ <ng-container [vListOutlet]="listHeaderDir.templateRef"></ng-container>
79
+ </ng-template>
80
+ }
81
+ @if (listFooterDir !== undefined) {
82
+ <ng-template vListFooter>
83
+ <ng-container [vListOutlet]="listFooterDir.templateRef"></ng-container>
84
+ </ng-template>
85
+ }
86
+ @if (listEmptyDir !== undefined) {
87
+ <ng-template vListEmpty>
88
+ <ng-container [vListOutlet]="listEmptyDir.templateRef"></ng-container>
89
+ </ng-template>
90
+ }
91
+ <!-- Item separator: unwrap each flattened entry back to its item -->
92
+ @if (itemSeparatorDir !== undefined) {
93
+ <ng-template
94
+ vListSeparator
95
+ let-highlighted
96
+ let-leadingItem="leadingItem"
97
+ let-trailingItem="trailingItem"
98
+ >
99
+ <ng-container
100
+ [vListOutlet]="itemSeparatorDir.templateRef"
101
+ [vListOutletContext]="
102
+ itemSeparatorContextOf(highlighted, leadingItem, trailingItem)
103
+ "
104
+ ></ng-container>
105
+ </ng-template>
106
+ }
107
+ </VirtualizedList>
108
+ `;
@@ -0,0 +1,18 @@
1
+ // Hermes has no `FinalizationRegistry`, and `@angular/core` >=22.2 builds one at module load
2
+ // Cleanup callbacks never fire, which only costs Angular's signal debug bookkeeping
3
+
4
+ class InertFinalizationRegistry {
5
+ register(): void {}
6
+
7
+ unregister(): boolean {
8
+ return false;
9
+ }
10
+ }
11
+
12
+ if (typeof globalThis.FinalizationRegistry === 'undefined') {
13
+ Object.defineProperty(globalThis, 'FinalizationRegistry', {
14
+ value: InertFinalizationRegistry,
15
+ configurable: true,
16
+ writable: true,
17
+ });
18
+ }
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  // template dependencies to importable names (NG3004, the same rule that put the element directives
18
18
  // back below). Retiring them means retiring them from those nine components first.
19
19
 
20
+ import './finalization-registry';
20
21
  import './register';
21
22
 
22
23
  export {
@@ -224,7 +225,17 @@ export type {
224
225
  ITaskCanceller,
225
226
  ITaskCancelProvider,
226
227
  } from './modules/app-registry';
227
- export { ColorSchemeService, WindowDimensionsService } from './services';
228
+ export {
229
+ ColorSchemeService,
230
+ WindowDimensionsService,
231
+ createResourceHook,
232
+ createEventValueHook,
233
+ connectWatchedSignal,
234
+ PermissionsServiceBase,
235
+ } from './services';
236
+ export { AccessibilityInputsBase } from './accessibility-inputs';
237
+ export { NativeViewBase } from './native-view-base';
238
+ export { anchorStyleProp } from './primitives/shared';
228
239
 
229
240
  // Framework-agnostic runtime modules from @symbiote-native/engine. Every adapter re-exports them so
230
241
  // app code names only @symbiote-native/angular.
@@ -0,0 +1,51 @@
1
+ import { Directive, ElementRef, inject, Input } from '@angular/core';
2
+ import { resolveAccessibilityProps } from '@symbiote-native/components';
3
+ import type { IDescriptor } from '@symbiote-native/components';
4
+ import type {
5
+ IStyleProp,
6
+ ISymbioteEvent,
7
+ IViewStyle,
8
+ } from '@symbiote-native/engine';
9
+ import { AccessibilityInputsBase } from './accessibility-inputs';
10
+ import { anchorStyleProp } from './primitives/shared';
11
+
12
+ // Shared shell of a component that paints one Expo native view: the common inputs, the anchor
13
+ // host `class="..."` resolves onto, and a descriptor that recomputes on every OnPush check
14
+ @Directive()
15
+ export abstract class NativeViewBase extends AccessibilityInputsBase {
16
+ @Input() style?: IStyleProp<IViewStyle>;
17
+ @Input() testID?: string;
18
+ @Input() nativeID?: string;
19
+ @Input() onLayout?: (event: ISymbioteEvent) => void;
20
+ @Input() onAccessibilityTap?: (event: ISymbioteEvent) => void;
21
+ @Input() onMagicTap?: (event: ISymbioteEvent) => void;
22
+ @Input() onAccessibilityEscape?: (event: ISymbioteEvent) => void;
23
+
24
+ // This component's own `ElementRef`, the anchor a `class="..."` at the use site lands on
25
+ private readonly elementRef = inject(ElementRef);
26
+
27
+ // Names of the component's own inputs that go to the native view, read off `this`
28
+ protected abstract readonly propNames: readonly string[];
29
+
30
+ // The core render fn, `null` when the view has nothing to paint on this platform
31
+ protected abstract renderView(props: object): IDescriptor | null;
32
+
33
+ get descriptor(): IDescriptor | null {
34
+ return this.renderView(
35
+ resolveAccessibilityProps({
36
+ ...this.accessibilityInputProps(),
37
+ ...Object.fromEntries(
38
+ this.propNames.map(name => [name, Reflect.get(this, name)]),
39
+ ),
40
+ // Ambient class style first, so an explicit `style` input wins
41
+ style: [anchorStyleProp<IViewStyle>(this.elementRef), this.style],
42
+ testID: this.testID,
43
+ nativeID: this.nativeID,
44
+ onLayout: this.onLayout,
45
+ onAccessibilityTap: this.onAccessibilityTap,
46
+ onMagicTap: this.onMagicTap,
47
+ onAccessibilityEscape: this.onAccessibilityEscape,
48
+ }),
49
+ );
50
+ }
51
+ }
@@ -29,6 +29,8 @@ import {
29
29
  ɵprovideZonelessChangeDetectionInternal as provideZonelessChangeDetectionInternal,
30
30
  type ComponentRef,
31
31
  type EnvironmentInjector,
32
+ type EnvironmentProviders,
33
+ type Provider,
32
34
  type Type,
33
35
  } from '@angular/core';
34
36
  import { SymbioteRendererFactory } from '../renderer';
@@ -119,6 +121,15 @@ export interface IMountOptions {
119
121
  wrapperComponent?: Type<unknown>;
120
122
  }
121
123
 
124
+ // App-level providers (`provideHttpClient()` and the like), set once at bootstrap
125
+ let rootProviders: (Provider | EnvironmentProviders)[] = [];
126
+
127
+ export function setRootProviders(
128
+ providers: (Provider | EnvironmentProviders)[],
129
+ ): void {
130
+ rootProviders = providers;
131
+ }
132
+
122
133
  function applyInputs(
123
134
  cmpRef: ComponentRef<unknown>,
124
135
  initialProps: object | undefined,
@@ -189,32 +200,17 @@ function settleAngularDevMode(): void {
189
200
  enableProdMode();
190
201
  }
191
202
 
192
- export function mount(
193
- rootTag: IRootTag,
194
- rootComponent: Type<unknown>,
195
- options?: IMountOptions,
196
- ): SymbioteSurface {
197
- settleAngularDevMode();
198
- // A re-mount on a live rootTag starts clean; otherwise the stale app double-drives the surface.
199
- teardown(rootTag);
200
-
201
- const surface = createSurface(rootTag);
202
- const rendererFactory = new SymbioteRendererFactory(surface);
203
- const injector = createEnvironmentInjector(
203
+ // Kept apart from `mount` so the provider list reads as one block
204
+ function createRootInjector(
205
+ surface: SymbioteSurface,
206
+ rendererFactory: SymbioteRendererFactory,
207
+ ): EnvironmentInjector {
208
+ return createEnvironmentInjector(
204
209
  [
205
- {
206
- provide: RendererFactory2,
207
- useValue: rendererFactory,
208
- },
209
- // `getElementById` is not decoration: `resource()` resolves TransferState, whose root
210
- // factory runs `retrieveTransferredState(doc, appId)` ->
211
- // `doc.getElementById(appId + '-state')` (core/src/transfer_state.ts:156) to pick up
212
- // server-rendered state. A stub without it makes that a TypeError, and because the throw
213
- // happens while the component is being constructed, the ENTIRE screen renders nothing —
214
- // white body under a native header that navigation drew anyway, no error surfaced. The
215
- // `optional: true` on the injector lookup does not help: the token resolves, its factory
216
- // is what throws. Returning null is the honest client answer — there is no server here, so
217
- // `script?.tagName` short-circuits and the caller gets `{}`.
210
+ ...rootProviders,
211
+ { provide: RendererFactory2, useValue: rendererFactory },
212
+ // `resource()` resolves TransferState, which calls `doc.getElementById`
213
+ // a stub without it throws mid-construction and the whole screen renders nothing
218
214
  {
219
215
  provide: DOCUMENT,
220
216
  useValue: {
@@ -223,72 +219,75 @@ export function mount(
223
219
  getElementById: (): null => null,
224
220
  },
225
221
  },
226
- // createEnvironmentInjector with a null parent scopes this injector to {'environment'}
227
- // only (see EnvironmentNgModuleRefAdapter), so providedIn:'root' tokens — ApplicationRef
228
- // included — never resolve (R3Injector.get walks up for a `scopes` containing 'root', and
229
- // a null parent always dead-ends in NullInjector). platform-browser solves this the same
230
- // way for real DOM apps via BROWSER_MODULE_PROVIDERS: hand { provide: INJECTOR_SCOPE,
231
- // useValue: 'root' } to the app-level providers, which R3Injector's constructor reads to
232
- // self-tag this.scopes with 'root'. Same trick here, no PlatformRef, no DOM.
222
+ // A null-parent injector is not root-scoped, so `providedIn: 'root'` tokens never resolve
233
223
  { provide: INJECTOR_SCOPE, useValue: 'root' },
234
- // Supplies the real ChangeDetectionSchedulerImpl (microtask-batched via
235
- // ApplicationRef.afterTick) + NoopNgZone + ZONELESS_ENABLED: true — the exact bundle
236
- // internalCreateApplication() uses. Replaces the old unconditional
237
- // `rootView.detectChanges(); cmpView.detectChanges()` (force-ran both root views on every
238
- // tick) with Angular's own tick(), which only enters a view something actually marked
239
- // dirty. This does NOT stop the root's own template from re-running on a plain press or
240
- // `markForCheck()` anywhere in the tree — `markViewDirty` unconditionally sets RefreshView
241
- // on every ancestor up to the root; that's fundamental Angular zoneless behavior, not
242
- // something this swap changes. A genuine child `@Component` boundary still protects a
243
- // sibling branch from an unrelated press.
224
+ // Real microtask-batched scheduler plus a noop zone, so `appRef.tick()` drives the views
244
225
  ...provideZonelessChangeDetectionInternal(),
245
- // Angular's INTERNAL_APPLICATION_ERROR_HANDLER reports a tick() exception via
246
- // `injector.get(ErrorHandler)`; a normal `bootstrapApplication` registers that token by
247
- // default, but our from-scratch environment injector never did, so the lookup itself threw
248
- // NG0201 and replaced the real error with "No provider found for ErrorHandler" — any async
249
- // tick() exception crashed hard, uncaught, instead of being reported. The token still has to
250
- // be provided for exactly that reason; what it resolves to is now SymbioteErrorHandler
251
- // (see above), which reports through the engine instead of Angular's `console.error('ERROR',
252
- // e)` and, like the default, returns rather than rethrows so the app keeps running.
226
+ // Angular's `tick()` looks up `ErrorHandler`, and a from-scratch injector has none (NG0201)
253
227
  { provide: ErrorHandler, useClass: SymbioteErrorHandler },
254
228
  ColorSchemeService,
255
229
  WindowDimensionsService,
256
230
  ],
257
231
  rootInjectorParent(),
258
232
  );
233
+ }
259
234
 
260
- // hostElement = the surface: the component's template content commits straight into the
261
- // surface with no wrapper view, the engine wrapping surface.children in its synthetic flex
262
- // root — the Angular equivalent of Vue's `app.mount(surface)`.
263
- let cmpRef: ComponentRef<unknown>;
264
- let rootRef: ComponentRef<unknown> | undefined;
265
- if (options?.wrapperComponent === undefined) {
266
- cmpRef = createComponent<unknown>(rootComponent, {
267
- environmentInjector: injector,
268
- hostElement: asAngularHost(surface),
269
- });
270
- applyInputs(cmpRef, options?.initialProps);
271
- } else {
272
- // The root gets its own host node from the renderer (createComponent without a
273
- // hostElement) but is not attached to the surface directly; it is handed to the
274
- // wrapper as projectable content instead, so only the wrapper needs a real host.
275
- const rootHost = asAngularHost(createDetachedViewHost());
276
- rootRef = createComponent<unknown>(rootComponent, {
277
- environmentInjector: injector,
278
- hostElement: rootHost,
279
- });
280
- applyInputs(rootRef, options.initialProps);
281
- cmpRef = createComponent<unknown>(options.wrapperComponent, {
235
+ interface IRoots {
236
+ cmpRef: ComponentRef<unknown>;
237
+ rootRef: ComponentRef<unknown> | undefined;
238
+ }
239
+
240
+ function createRoots(
241
+ injector: EnvironmentInjector,
242
+ surface: SymbioteSurface,
243
+ rootComponent: Type<unknown>,
244
+ options: IMountOptions | undefined,
245
+ ): IRoots {
246
+ const { wrapperComponent, initialProps } = options ?? {};
247
+ // `hostElement` is the surface, content commits straight into it with no wrapper view
248
+ if (wrapperComponent === undefined) {
249
+ const cmpRef = createComponent<unknown>(rootComponent, {
282
250
  environmentInjector: injector,
283
251
  hostElement: asAngularHost(surface),
284
- projectableNodes: [[rootHost]],
285
252
  });
253
+ applyInputs(cmpRef, initialProps);
254
+ return { cmpRef, rootRef: undefined };
286
255
  }
256
+ // The root gets its own host and reaches the wrapper as projected content
257
+ const rootHost = asAngularHost(createDetachedViewHost());
258
+ const rootRef = createComponent<unknown>(rootComponent, {
259
+ environmentInjector: injector,
260
+ hostElement: rootHost,
261
+ });
262
+ applyInputs(rootRef, initialProps);
263
+ const cmpRef = createComponent<unknown>(wrapperComponent, {
264
+ environmentInjector: injector,
265
+ hostElement: asAngularHost(surface),
266
+ projectableNodes: [[rootHost]],
267
+ });
268
+ return { cmpRef, rootRef };
269
+ }
270
+
271
+ export function mount(
272
+ rootTag: IRootTag,
273
+ rootComponent: Type<unknown>,
274
+ options?: IMountOptions,
275
+ ): SymbioteSurface {
276
+ settleAngularDevMode();
277
+ // A re-mount on a live rootTag starts clean; otherwise the stale app double-drives the surface.
278
+ teardown(rootTag);
279
+
280
+ const surface = createSurface(rootTag);
281
+ const rendererFactory = new SymbioteRendererFactory(surface);
282
+ const injector = createRootInjector(surface, rendererFactory);
283
+ const { cmpRef, rootRef } = createRoots(
284
+ injector,
285
+ surface,
286
+ rootComponent,
287
+ options,
288
+ );
287
289
 
288
- // Attach both root views to ApplicationRef so its own tick() (via the real
289
- // ChangeDetectionSchedulerImpl provided above) drives them from here on — no manual
290
- // ChangeDetectorRef juggling needed. markForCheck anywhere in the tree now notifies
291
- // the real scheduler, which batches a microtask and calls appRef.tick() itself.
290
+ // `tick()` then drives both root views through the real scheduler
292
291
  const appRef = injector.get(ApplicationRef);
293
292
  appRef.attachView(cmpRef.hostView);
294
293
  if (rootRef !== undefined) {
@@ -0,0 +1,15 @@
1
+ // A signal fed by a `watch` subscription that lives as long as the injector's owner
2
+
3
+ import { effect, signal, type Injector, type Signal } from '@angular/core';
4
+
5
+ export function connectWatchedSignal<T>(
6
+ injector: Injector,
7
+ initial: T,
8
+ watch: (set: (value: T) => void) => () => void,
9
+ ): Signal<T> {
10
+ const value = signal(initial);
11
+
12
+ effect(onCleanup => onCleanup(watch(next => value.set(next))), { injector });
13
+
14
+ return value.asReadonly();
15
+ }
@@ -0,0 +1,24 @@
1
+ // Every `useXStatus`-style `injectX` (`injectAudioPlayerStatus`, ...) binds this to its own
2
+ // event name + initial-value getter instead of repeating the signal/effect wiring
3
+
4
+ import { effect, signal, type Signal } from '@angular/core';
5
+ import type { IEventValueSource } from '@symbiote-native/engine';
6
+
7
+ export function createEventValueHook<
8
+ TSource extends IEventValueSource<TValue, TEvent>,
9
+ TValue,
10
+ TEvent extends string = string,
11
+ >(event: TEvent, getValue: (source: TSource) => TValue) {
12
+ return function injectValue(getSource: () => TSource): Signal<TValue> {
13
+ const value = signal(getValue(getSource()));
14
+
15
+ effect(onCleanup => {
16
+ const source = getSource();
17
+ value.set(getValue(source));
18
+ const subscription = source.addListener(event, next => value.set(next));
19
+ onCleanup(() => subscription.remove());
20
+ });
21
+
22
+ return value.asReadonly();
23
+ };
24
+ }
@@ -0,0 +1,32 @@
1
+ // Every `useReleasingSharedObject`-style `injectX` (`injectAudioPlayer`, `injectImageManipulator`,
2
+ // ...) binds this to its own `createXController()` factory instead of repeating the wiring
3
+
4
+ import { DestroyRef, effect, inject, signal, type Signal } from '@angular/core';
5
+
6
+ export type IResourceHookController<TArgs extends unknown[], TResource> = {
7
+ resolve: (...args: TArgs) => TResource;
8
+ flushDispose: () => void;
9
+ dispose: () => void;
10
+ };
11
+
12
+ export function createResourceHook<TArgs extends unknown[], TResource>(
13
+ createController: () => IResourceHookController<TArgs, TResource>,
14
+ ) {
15
+ return function injectResource(getArgs: () => TArgs): Signal<TResource> {
16
+ const controller = createController();
17
+ const resource = signal(controller.resolve(...getArgs()));
18
+
19
+ effect(() => {
20
+ resource.set(controller.resolve(...getArgs()));
21
+ });
22
+
23
+ effect(() => {
24
+ resource();
25
+ controller.flushDispose();
26
+ });
27
+
28
+ inject(DestroyRef).onDestroy(() => controller.dispose());
29
+
30
+ return resource.asReadonly();
31
+ };
32
+ }
@@ -1,2 +1,6 @@
1
1
  export { ColorSchemeService } from './color-scheme.service';
2
2
  export { WindowDimensionsService } from './window-dimensions.service';
3
+ export { createResourceHook } from './create-resource-hook';
4
+ export { createEventValueHook } from './create-event-value-hook';
5
+ export { connectWatchedSignal } from './connect-watched-signal';
6
+ export { PermissionsServiceBase } from './permissions-service-base';
@@ -0,0 +1,46 @@
1
+ import { signal, type Signal } from '@angular/core';
2
+
3
+ // Общая часть сервисов разрешений всех Expo-пакетов, наследник передаёт пару методов
4
+ // Сбой автозапроса попадает в `error`, а не в unhandled rejection: null-статус с непустым `error`
5
+ // значит "запрос упал", а не "ещё не было"
6
+ export abstract class PermissionsServiceBase<TPermission, TOptions = void> {
7
+ private readonly status = signal<TPermission | null>(null);
8
+ private readonly fetchError = signal<Error | null>(null);
9
+ private isAutoFetchStarted = false;
10
+
11
+ readonly error: Signal<Error | null> = this.fetchError.asReadonly();
12
+
13
+ protected constructor(
14
+ private readonly getMethod: (options?: TOptions) => Promise<TPermission>,
15
+ private readonly requestMethod: (
16
+ options?: TOptions,
17
+ ) => Promise<TPermission>,
18
+ ) {}
19
+
20
+ // Защёлка, а не проверка `status() === null`: упавший запрос оставляет null навсегда
21
+ connect(): Signal<TPermission | null> {
22
+ if (!this.isAutoFetchStarted) {
23
+ this.isAutoFetchStarted = true;
24
+ this.get().catch((cause: unknown) => {
25
+ this.fetchError.set(
26
+ cause instanceof Error ? cause : new Error(String(cause)),
27
+ );
28
+ });
29
+ }
30
+ return this.status.asReadonly();
31
+ }
32
+
33
+ async get(options?: TOptions): Promise<TPermission> {
34
+ return this.store(await this.getMethod(options));
35
+ }
36
+
37
+ async request(options?: TOptions): Promise<TPermission> {
38
+ return this.store(await this.requestMethod(options));
39
+ }
40
+
41
+ private store(response: TPermission): TPermission {
42
+ this.status.set(response);
43
+ this.fetchError.set(null);
44
+ return response;
45
+ }
46
+ }