@symbiote-native/angular 0.1.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/LICENSE +21 -0
- package/README.md +147 -0
- package/build/angular/components/activity-indicator/index.android.d.ts +12 -0
- package/build/angular/components/activity-indicator/index.android.js +21 -0
- package/build/angular/components/activity-indicator/index.android.js.map +1 -0
- package/build/angular/components/activity-indicator/index.d.ts +1 -0
- package/build/angular/components/activity-indicator/index.ios.d.ts +8 -0
- package/build/angular/components/activity-indicator/index.ios.js +21 -0
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -0
- package/build/angular/components/activity-indicator/index.js +4 -0
- package/build/angular/components/activity-indicator/index.js.map +1 -0
- package/build/angular/components/activity-indicator/shared.d.ts +67 -0
- package/build/angular/components/activity-indicator/shared.js +260 -0
- package/build/angular/components/activity-indicator/shared.js.map +1 -0
- package/build/angular/components/button.d.ts +63 -0
- package/build/angular/components/button.js +332 -0
- package/build/angular/components/button.js.map +1 -0
- package/build/angular/components/drawer-layout-android/index.android.d.ts +34 -0
- package/build/angular/components/drawer-layout-android/index.android.js +291 -0
- package/build/angular/components/drawer-layout-android/index.android.js.map +1 -0
- package/build/angular/components/drawer-layout-android/index.d.ts +11 -0
- package/build/angular/components/drawer-layout-android/index.js +109 -0
- package/build/angular/components/drawer-layout-android/index.js.map +1 -0
- package/build/angular/components/drawer-layout-android/shared.d.ts +74 -0
- package/build/angular/components/drawer-layout-android/shared.js +138 -0
- package/build/angular/components/drawer-layout-android/shared.js.map +1 -0
- package/build/angular/components/flat-list/index.d.ts +122 -0
- package/build/angular/components/flat-list/index.js +689 -0
- package/build/angular/components/flat-list/index.js.map +1 -0
- package/build/angular/components/image/index.android.d.ts +10 -0
- package/build/angular/components/image/index.android.js +59 -0
- package/build/angular/components/image/index.android.js.map +1 -0
- package/build/angular/components/image/index.d.ts +1 -0
- package/build/angular/components/image/index.ios.d.ts +10 -0
- package/build/angular/components/image/index.ios.js +59 -0
- package/build/angular/components/image/index.ios.js.map +1 -0
- package/build/angular/components/image/index.js +3 -0
- package/build/angular/components/image/index.js.map +1 -0
- package/build/angular/components/image/shared.d.ts +101 -0
- package/build/angular/components/image/shared.js +408 -0
- package/build/angular/components/image/shared.js.map +1 -0
- package/build/angular/components/image-background.d.ts +81 -0
- package/build/angular/components/image-background.js +418 -0
- package/build/angular/components/image-background.js.map +1 -0
- package/build/angular/components/input-accessory-view.d.ts +59 -0
- package/build/angular/components/input-accessory-view.js +266 -0
- package/build/angular/components/input-accessory-view.js.map +1 -0
- package/build/angular/components/keyboard-avoiding-view.d.ts +82 -0
- package/build/angular/components/keyboard-avoiding-view.js +363 -0
- package/build/angular/components/keyboard-avoiding-view.js.map +1 -0
- package/build/angular/components/modal.d.ts +92 -0
- package/build/angular/components/modal.js +365 -0
- package/build/angular/components/modal.js.map +1 -0
- package/build/angular/components/pressable/index.d.ts +123 -0
- package/build/angular/components/pressable/index.js +421 -0
- package/build/angular/components/pressable/index.js.map +1 -0
- package/build/angular/components/refresh-control.d.ts +86 -0
- package/build/angular/components/refresh-control.js +347 -0
- package/build/angular/components/refresh-control.js.map +1 -0
- package/build/angular/components/safe-area-view.d.ts +57 -0
- package/build/angular/components/safe-area-view.js +253 -0
- package/build/angular/components/safe-area-view.js.map +1 -0
- package/build/angular/components/scroll-view/index.android.d.ts +21 -0
- package/build/angular/components/scroll-view/index.android.js +235 -0
- package/build/angular/components/scroll-view/index.android.js.map +1 -0
- package/build/angular/components/scroll-view/index.d.ts +3 -0
- package/build/angular/components/scroll-view/index.ios.d.ts +9 -0
- package/build/angular/components/scroll-view/index.ios.js +86 -0
- package/build/angular/components/scroll-view/index.ios.js.map +1 -0
- package/build/angular/components/scroll-view/index.js +6 -0
- package/build/angular/components/scroll-view/index.js.map +1 -0
- package/build/angular/components/scroll-view/projection.d.ts +32 -0
- package/build/angular/components/scroll-view/projection.js +312 -0
- package/build/angular/components/scroll-view/projection.js.map +1 -0
- package/build/angular/components/scroll-view/shared.d.ts +230 -0
- package/build/angular/components/scroll-view/shared.js +679 -0
- package/build/angular/components/scroll-view/shared.js.map +1 -0
- package/build/angular/components/scroll-view/sticky-header.d.ts +41 -0
- package/build/angular/components/scroll-view/sticky-header.js +208 -0
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -0
- package/build/angular/components/section-list/index.d.ts +116 -0
- package/build/angular/components/section-list/index.js +641 -0
- package/build/angular/components/section-list/index.js.map +1 -0
- package/build/angular/components/switch/index.android.d.ts +20 -0
- package/build/angular/components/switch/index.android.js +70 -0
- package/build/angular/components/switch/index.android.js.map +1 -0
- package/build/angular/components/switch/index.d.ts +1 -0
- package/build/angular/components/switch/index.ios.d.ts +19 -0
- package/build/angular/components/switch/index.ios.js +69 -0
- package/build/angular/components/switch/index.ios.js.map +1 -0
- package/build/angular/components/switch/index.js +4 -0
- package/build/angular/components/switch/index.js.map +1 -0
- package/build/angular/components/switch/shared.d.ts +40 -0
- package/build/angular/components/switch/shared.js +127 -0
- package/build/angular/components/switch/shared.js.map +1 -0
- package/build/angular/components/text-input.d.ts +153 -0
- package/build/angular/components/text-input.js +604 -0
- package/build/angular/components/text-input.js.map +1 -0
- package/build/angular/components/touchable-native-feedback.d.ts +83 -0
- package/build/angular/components/touchable-native-feedback.js +376 -0
- package/build/angular/components/touchable-native-feedback.js.map +1 -0
- package/build/angular/components/touchable.d.ts +248 -0
- package/build/angular/components/touchable.js +1206 -0
- package/build/angular/components/touchable.js.map +1 -0
- package/build/angular/components/virtualized-list/directives.d.ts +54 -0
- package/build/angular/components/virtualized-list/directives.js +154 -0
- package/build/angular/components/virtualized-list/directives.js.map +1 -0
- package/build/angular/components/virtualized-list/index.d.ts +268 -0
- package/build/angular/components/virtualized-list/index.js +1162 -0
- package/build/angular/components/virtualized-list/index.js.map +1 -0
- package/build/angular/components/virtualized-section-list/directives.d.ts +37 -0
- package/build/angular/components/virtualized-section-list/directives.js +77 -0
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -0
- package/build/angular/components/virtualized-section-list/index.d.ts +178 -0
- package/build/angular/components/virtualized-section-list/index.js +722 -0
- package/build/angular/components/virtualized-section-list/index.js.map +1 -0
- package/build/angular/components.d.ts +39 -0
- package/build/angular/components.js +38 -0
- package/build/angular/components.js.map +1 -0
- package/build/angular/create-portal.d.ts +22 -0
- package/build/angular/create-portal.js +74 -0
- package/build/angular/create-portal.js.map +1 -0
- package/build/angular/create-tunnel.d.ts +34 -0
- package/build/angular/create-tunnel.js +126 -0
- package/build/angular/create-tunnel.js.map +1 -0
- package/build/angular/descriptor-to-angular/index.d.ts +22 -0
- package/build/angular/descriptor-to-angular/index.js +142 -0
- package/build/angular/descriptor-to-angular/index.js.map +1 -0
- package/build/angular/host-instance/index.d.ts +2 -0
- package/build/angular/host-instance/index.js +37 -0
- package/build/angular/host-instance/index.js.map +1 -0
- package/build/angular/index.d.ts +21 -0
- package/build/angular/index.js +42 -0
- package/build/angular/index.js.map +1 -0
- package/build/angular/modules/animated/create-animated-component.d.ts +68 -0
- package/build/angular/modules/animated/create-animated-component.js +429 -0
- package/build/angular/modules/animated/create-animated-component.js.map +1 -0
- package/build/angular/modules/animated/index.d.ts +51 -0
- package/build/angular/modules/animated/index.js +59 -0
- package/build/angular/modules/animated/index.js.map +1 -0
- package/build/angular/modules/app-registry/index.d.ts +7 -0
- package/build/angular/modules/app-registry/index.js +23 -0
- package/build/angular/modules/app-registry/index.js.map +1 -0
- package/build/angular/modules/status-bar.d.ts +20 -0
- package/build/angular/modules/status-bar.js +59 -0
- package/build/angular/modules/status-bar.js.map +1 -0
- package/build/angular/primitives/index.d.ts +63 -0
- package/build/angular/primitives/index.js +189 -0
- package/build/angular/primitives/index.js.map +1 -0
- package/build/angular/primitives/shared.d.ts +89 -0
- package/build/angular/primitives/shared.js +172 -0
- package/build/angular/primitives/shared.js.map +1 -0
- package/build/angular/render.d.ts +11 -0
- package/build/angular/render.js +161 -0
- package/build/angular/render.js.map +1 -0
- package/build/angular/renderer.d.ts +38 -0
- package/build/angular/renderer.js +360 -0
- package/build/angular/renderer.js.map +1 -0
- package/build/angular/services/color-scheme.service.d.ts +16 -0
- package/build/angular/services/color-scheme.service.js +31 -0
- package/build/angular/services/color-scheme.service.js.map +1 -0
- package/build/angular/services/index.d.ts +2 -0
- package/build/angular/services/index.js +3 -0
- package/build/angular/services/index.js.map +1 -0
- package/build/angular/services/window-dimensions.service.d.ts +16 -0
- package/build/angular/services/window-dimensions.service.js +42 -0
- package/build/angular/services/window-dimensions.service.js.map +1 -0
- package/build/components/activity-indicator/index.android.d.ts +9 -0
- package/build/components/activity-indicator/index.android.js +64 -0
- package/build/components/activity-indicator/index.d.ts +1 -0
- package/build/components/activity-indicator/index.ios.d.ts +5 -0
- package/build/components/activity-indicator/index.ios.js +64 -0
- package/build/components/activity-indicator/index.js +3 -0
- package/build/components/activity-indicator/shared.d.ts +64 -0
- package/build/components/activity-indicator/shared.js +424 -0
- package/build/components/button.d.ts +60 -0
- package/build/components/button.js +456 -0
- package/build/components/drawer-layout-android/index.android.d.ts +29 -0
- package/build/components/drawer-layout-android/index.android.js +326 -0
- package/build/components/drawer-layout-android/index.d.ts +8 -0
- package/build/components/drawer-layout-android/index.js +145 -0
- package/build/components/drawer-layout-android/shared.d.ts +71 -0
- package/build/components/drawer-layout-android/shared.js +205 -0
- package/build/components/flat-list/index.d.ts +119 -0
- package/build/components/flat-list/index.js +692 -0
- package/build/components/image/index.android.d.ts +7 -0
- package/build/components/image/index.android.js +88 -0
- package/build/components/image/index.d.ts +1 -0
- package/build/components/image/index.ios.d.ts +7 -0
- package/build/components/image/index.ios.js +88 -0
- package/build/components/image/index.js +2 -0
- package/build/components/image/shared.d.ts +101 -0
- package/build/components/image/shared.js +407 -0
- package/build/components/image-background.d.ts +78 -0
- package/build/components/image-background.js +583 -0
- package/build/components/input-accessory-view.d.ts +56 -0
- package/build/components/input-accessory-view.js +407 -0
- package/build/components/keyboard-avoiding-view.d.ts +79 -0
- package/build/components/keyboard-avoiding-view.js +509 -0
- package/build/components/modal.d.ts +89 -0
- package/build/components/modal.js +534 -0
- package/build/components/pressable/index.d.ts +120 -0
- package/build/components/pressable/index.js +640 -0
- package/build/components/refresh-control.d.ts +83 -0
- package/build/components/refresh-control.js +514 -0
- package/build/components/safe-area-view.d.ts +54 -0
- package/build/components/safe-area-view.js +393 -0
- package/build/components/scroll-view/index.android.d.ts +16 -0
- package/build/components/scroll-view/index.android.js +228 -0
- package/build/components/scroll-view/index.d.ts +3 -0
- package/build/components/scroll-view/index.ios.d.ts +6 -0
- package/build/components/scroll-view/index.ios.js +113 -0
- package/build/components/scroll-view/index.js +5 -0
- package/build/components/scroll-view/projection.d.ts +32 -0
- package/build/components/scroll-view/projection.js +311 -0
- package/build/components/scroll-view/shared.d.ts +225 -0
- package/build/components/scroll-view/shared.js +757 -0
- package/build/components/scroll-view/sticky-header.d.ts +38 -0
- package/build/components/scroll-view/sticky-header.js +257 -0
- package/build/components/section-list/index.d.ts +113 -0
- package/build/components/section-list/index.js +768 -0
- package/build/components/switch/index.android.d.ts +17 -0
- package/build/components/switch/index.android.js +107 -0
- package/build/components/switch/index.d.ts +1 -0
- package/build/components/switch/index.ios.d.ts +16 -0
- package/build/components/switch/index.ios.js +106 -0
- package/build/components/switch/index.js +3 -0
- package/build/components/switch/shared.d.ts +37 -0
- package/build/components/switch/shared.js +176 -0
- package/build/components/text-input.d.ts +150 -0
- package/build/components/text-input.js +826 -0
- package/build/components/touchable-native-feedback.d.ts +80 -0
- package/build/components/touchable-native-feedback.js +550 -0
- package/build/components/touchable.d.ts +241 -0
- package/build/components/touchable.js +1672 -0
- package/build/components/virtualized-list/directives.d.ts +41 -0
- package/build/components/virtualized-list/directives.js +250 -0
- package/build/components/virtualized-list/index.d.ts +265 -0
- package/build/components/virtualized-list/index.js +1344 -0
- package/build/components/virtualized-section-list/directives.d.ts +28 -0
- package/build/components/virtualized-section-list/directives.js +149 -0
- package/build/components/virtualized-section-list/index.d.ts +175 -0
- package/build/components/virtualized-section-list/index.js +854 -0
- package/build/components.d.ts +39 -0
- package/build/components.js +37 -0
- package/build/create-portal.d.ts +17 -0
- package/build/create-portal.js +128 -0
- package/build/create-tunnel.d.ts +29 -0
- package/build/create-tunnel.js +181 -0
- package/build/descriptor-to-angular/index.d.ts +19 -0
- package/build/descriptor-to-angular/index.js +186 -0
- package/build/host-instance/index.d.ts +2 -0
- package/build/host-instance/index.js +36 -0
- package/build/index.d.ts +21 -0
- package/build/index.js +41 -0
- package/build/modules/animated/create-animated-component.d.ts +57 -0
- package/build/modules/animated/create-animated-component.js +490 -0
- package/build/modules/animated/index.d.ts +51 -0
- package/build/modules/animated/index.js +58 -0
- package/build/modules/app-registry/index.d.ts +7 -0
- package/build/modules/app-registry/index.js +22 -0
- package/build/modules/status-bar.d.ts +17 -0
- package/build/modules/status-bar.js +121 -0
- package/build/primitives/index.d.ts +32 -0
- package/build/primitives/index.js +386 -0
- package/build/primitives/shared.d.ts +84 -0
- package/build/primitives/shared.js +231 -0
- package/build/render.d.ts +11 -0
- package/build/render.js +160 -0
- package/build/renderer.d.ts +38 -0
- package/build/renderer.js +359 -0
- package/build/services/color-scheme.service.d.ts +13 -0
- package/build/services/color-scheme.service.js +72 -0
- package/build/services/index.d.ts +2 -0
- package/build/services/index.js +2 -0
- package/build/services/window-dimensions.service.d.ts +13 -0
- package/build/services/window-dimensions.service.js +83 -0
- package/metro-css-parser.cjs +11 -0
- package/package.json +57 -0
- package/src/__tests__/responder-change-detection.test.ts +102 -0
- package/src/__tests__/responder-nested-cd.test.ts +113 -0
- package/src/angular-gaps.test.ts +142 -0
- package/src/angular-imports.test.ts +100 -0
- package/src/components/activity-indicator/activity-indicator-class-style.test.ts +44 -0
- package/src/components/activity-indicator/activity-indicator.test.ts +131 -0
- package/src/components/activity-indicator/index.android.ts +16 -0
- package/src/components/activity-indicator/index.ios.ts +17 -0
- package/src/components/activity-indicator/index.ts +4 -0
- package/src/components/activity-indicator/shared.ts +206 -0
- package/src/components/button.ts +193 -0
- package/src/components/flat-list/column-wrapper-style-class.test.ts +116 -0
- package/src/components/flat-list/flat-list-array-style.test.ts +131 -0
- package/src/components/flat-list/flat-list-class-style.test.ts +74 -0
- package/src/components/flat-list/flat-list-scroll-containment.test.ts +107 -0
- package/src/components/flat-list/flat-list.test.ts +99 -0
- package/src/components/flat-list/index.ts +575 -0
- package/src/components/image/image-class-style.test.ts +46 -0
- package/src/components/image/index.android.ts +48 -0
- package/src/components/image/index.ios.ts +48 -0
- package/src/components/image/index.ts +3 -0
- package/src/components/image/shared.ts +478 -0
- package/src/components/image-background-class-style.test.ts +48 -0
- package/src/components/image-background-style-class.test.ts +79 -0
- package/src/components/image-background.ts +270 -0
- package/src/components/input-accessory-view.test.ts +43 -0
- package/src/components/input-accessory-view.ts +191 -0
- package/src/components/keyboard-avoiding-view.test.ts +54 -0
- package/src/components/keyboard-avoiding-view.ts +308 -0
- package/src/components/modal.test.ts +43 -0
- package/src/components/modal.ts +304 -0
- package/src/components/pressable/index.ts +371 -0
- package/src/components/pressable/pressable.test.ts +85 -0
- package/src/components/refresh-control.test.ts +41 -0
- package/src/components/refresh-control.ts +288 -0
- package/src/components/safe-area-view.test.ts +43 -0
- package/src/components/safe-area-view.ts +178 -0
- package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +123 -0
- package/src/components/scroll-view/content-container-style-class.test.ts +75 -0
- package/src/components/scroll-view/index.android.ts +190 -0
- package/src/components/scroll-view/index.ios.ts +79 -0
- package/src/components/scroll-view/index.ts +7 -0
- package/src/components/scroll-view/projection.ts +384 -0
- package/src/components/scroll-view/scroll-view-class-style.test.ts +134 -0
- package/src/components/scroll-view/scroll-view-projection.test.ts +194 -0
- package/src/components/scroll-view/shared.ts +836 -0
- package/src/components/scroll-view/sticky-header.ts +230 -0
- package/src/components/section-list/index.ts +445 -0
- package/src/components/switch/android-switch-class.test.ts +42 -0
- package/src/components/switch/index.android.ts +61 -0
- package/src/components/switch/index.ios.ts +60 -0
- package/src/components/switch/index.ts +4 -0
- package/src/components/switch/shared.ts +197 -0
- package/src/components/switch/switch.test.ts +92 -0
- package/src/components/text-input-class-style.test.ts +54 -0
- package/src/components/text-input.ts +536 -0
- package/src/components/touchable-native-feedback.test.ts +49 -0
- package/src/components/touchable-native-feedback.ts +260 -0
- package/src/components/touchable.test.ts +102 -0
- package/src/components/touchable.ts +643 -0
- package/src/components/virtualized-list/directives.ts +169 -0
- package/src/components/virtualized-list/index.ts +1204 -0
- package/src/components/virtualized-list/virtualized-list-class-style.test.ts +76 -0
- package/src/components/virtualized-section-list/directives.ts +91 -0
- package/src/components/virtualized-section-list/index.ts +598 -0
- package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +60 -0
- package/src/components.ts +149 -0
- package/src/create-portal.test.ts +90 -0
- package/src/create-portal.ts +74 -0
- package/src/create-tunnel.test.ts +120 -0
- package/src/create-tunnel.ts +136 -0
- package/src/descriptor-to-angular/descriptor-outlet.test.ts +94 -0
- package/src/descriptor-to-angular/index.ts +190 -0
- package/src/host-instance/host-instance.test.ts +49 -0
- package/src/host-instance/index.ts +36 -0
- package/src/index.ts +257 -0
- package/src/modules/animated/animated-scroll-view.test.ts +85 -0
- package/src/modules/animated/create-animated-component.ts +435 -0
- package/src/modules/animated/index.ts +105 -0
- package/src/modules/app-registry/app-registry.test.ts +106 -0
- package/src/modules/app-registry/index.ts +60 -0
- package/src/modules/status-bar.test.ts +50 -0
- package/src/modules/status-bar.ts +57 -0
- package/src/primitives/index.ts +119 -0
- package/src/primitives/shared.ts +191 -0
- package/src/render.test.ts +147 -0
- package/src/render.ts +224 -0
- package/src/renderer.test.ts +179 -0
- package/src/renderer.ts +407 -0
- package/src/services/color-scheme.service.ts +27 -0
- package/src/services/index.ts +2 -0
- package/src/services/services.test.ts +163 -0
- package/src/services/window-dimensions.service.ts +41 -0
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// An Angular custom renderer over @symbiote-native/engine. Angular components never touch the
|
|
2
|
+
// DOM directly — every paint goes through Renderer2 (created per-component by
|
|
3
|
+
// RendererFactory2). We provide OUR factory, so each Renderer2 method maps onto the
|
|
4
|
+
// engine's tiny mutation API; the engine owns all Fabric clone-on-write, shared with
|
|
5
|
+
// every other adapter. This is the Angular twin of adapters/vue/src/renderer.ts and the
|
|
6
|
+
// proof the core is framework-agnostic (R4). See the `angular-adapter` skill.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
appendChild,
|
|
10
|
+
createAnchor,
|
|
11
|
+
createElement,
|
|
12
|
+
createRawText,
|
|
13
|
+
dlog,
|
|
14
|
+
getExplicitStyle,
|
|
15
|
+
insertBefore,
|
|
16
|
+
isDebug,
|
|
17
|
+
isSymbioteNode,
|
|
18
|
+
removeChild,
|
|
19
|
+
routeProp,
|
|
20
|
+
setEventListener,
|
|
21
|
+
setText,
|
|
22
|
+
toPublicInstance,
|
|
23
|
+
RAW_TEXT_COMPONENT,
|
|
24
|
+
SymbioteSurface,
|
|
25
|
+
type ISymbioteNode,
|
|
26
|
+
} from '@symbiote-native/engine';
|
|
27
|
+
import { descriptorFor } from '@symbiote-native/components';
|
|
28
|
+
import type { Renderer2, RendererFactory2, RendererType2 } from '@angular/core';
|
|
29
|
+
import {
|
|
30
|
+
getScrollViewProjection,
|
|
31
|
+
removeScrollViewProjectedChild,
|
|
32
|
+
} from './components/scroll-view/projection';
|
|
33
|
+
|
|
34
|
+
// Angular host nodes are all SymbioteNode (elements, raw text, anchors). The mount
|
|
35
|
+
// container is the surface, so a parent can be either a node or the surface root.
|
|
36
|
+
type IHostNode = ISymbioteNode;
|
|
37
|
+
type IHostElement = ISymbioteNode | SymbioteSurface;
|
|
38
|
+
|
|
39
|
+
function isSurface(parent: IHostElement): parent is SymbioteSurface {
|
|
40
|
+
return parent instanceof SymbioteSurface;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isRawText(node: ISymbioteNode): boolean {
|
|
44
|
+
return node.component === RAW_TEXT_COMPONENT;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
48
|
+
return typeof value === 'object' && value !== null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Diagnostic-only: tags each anchor with a sequential id so log lines can tell distinct
|
|
52
|
+
// anchors apart (they all otherwise print the same generic '#anchor' component name).
|
|
53
|
+
// Gated behind isDebug() so a production build pays zero cost (no WeakMap writes).
|
|
54
|
+
let anchorDebugCounter = 0;
|
|
55
|
+
const anchorDebugIds = new WeakMap<ISymbioteNode, number>();
|
|
56
|
+
function tagAnchorForDebug(node: ISymbioteNode): ISymbioteNode {
|
|
57
|
+
if (isDebug()) {
|
|
58
|
+
anchorDebugCounter += 1;
|
|
59
|
+
anchorDebugIds.set(node, anchorDebugCounter);
|
|
60
|
+
}
|
|
61
|
+
return node;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Diagnostic identity string for a renderer node/surface — component name, or 'surface'.
|
|
65
|
+
// Null-tolerant: a diagnostic must never throw (Angular hands insertBefore a null refChild).
|
|
66
|
+
function describeHost(node: IHostElement | null | undefined): string {
|
|
67
|
+
if (node === null || node === undefined) return 'null';
|
|
68
|
+
if (isSurface(node)) return 'surface';
|
|
69
|
+
const anchorId = anchorDebugIds.get(node);
|
|
70
|
+
return anchorId !== undefined ? `${node.component}#${anchorId}` : node.component;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const PRIMITIVE_SELECTOR_ALIAS: Record<string, string> = {
|
|
74
|
+
// Public ergonomic selectors map directly to the engine primitive descriptors.
|
|
75
|
+
View: 'symbiote-view',
|
|
76
|
+
Text: 'symbiote-text',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// A composed component's selector must be listed so createElement gives it a non-painting
|
|
80
|
+
// anchor host (see the set comment below). Tests register their own child-component selectors
|
|
81
|
+
// through this at module load; app-authored composed components can too.
|
|
82
|
+
export function registerComposedComponent(selector: string): void {
|
|
83
|
+
ANCHOR_HOST_COMPONENTS.add(selector);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ANCHOR_HOST_COMPONENTS: Set<string> = new Set([
|
|
87
|
+
// Composed Angular components render their real Fabric descriptor tree from the template;
|
|
88
|
+
// their Angular host element is only a framework bookkeeping node and must not paint. This
|
|
89
|
+
// is NOT limited to adapter-authored components — ANY custom composed @Component used as a
|
|
90
|
+
// plain <Tag> inside another template needs its selector listed here too, adapter or app
|
|
91
|
+
// code alike, or Angular's automatic host-element creation for it falls through to a raw
|
|
92
|
+
// Fabric createNode call with an unrecognized view name, which paints RN's own "Unimplemented
|
|
93
|
+
// component: <Tag>" fallback view instead (a real device-visible bug, not a silent no-op —
|
|
94
|
+
// discovered when examples/angular's own new demo components hit this).
|
|
95
|
+
'AccessibilityDemo',
|
|
96
|
+
'ActivityIndicator',
|
|
97
|
+
'AnimatedDemo',
|
|
98
|
+
'AnimatedParityDemo',
|
|
99
|
+
'Button',
|
|
100
|
+
'FlatList',
|
|
101
|
+
'NativeModulesDemo',
|
|
102
|
+
'ParityDemo',
|
|
103
|
+
'PlatformColorDemo',
|
|
104
|
+
'RefApiDemo',
|
|
105
|
+
'ResponderDemo',
|
|
106
|
+
'Slider',
|
|
107
|
+
'AnimatedView',
|
|
108
|
+
'symbiote-animated-view',
|
|
109
|
+
'AnimatedText',
|
|
110
|
+
'symbiote-animated-text',
|
|
111
|
+
'AnimatedImage',
|
|
112
|
+
'symbiote-animated-image',
|
|
113
|
+
'AnimatedScrollView',
|
|
114
|
+
'symbiote-animated-scroll-view',
|
|
115
|
+
'symbiote-descriptor-outlet',
|
|
116
|
+
'tunnel-out',
|
|
117
|
+
'Image',
|
|
118
|
+
'ImageBackground',
|
|
119
|
+
'InputAccessoryView',
|
|
120
|
+
'KeyboardAvoidingView',
|
|
121
|
+
'Modal',
|
|
122
|
+
'Pressable',
|
|
123
|
+
'RefreshControl',
|
|
124
|
+
'SafeAreaView',
|
|
125
|
+
'ScrollView',
|
|
126
|
+
'ScrollViewStickyHeader',
|
|
127
|
+
'SectionList',
|
|
128
|
+
'symbiote-sticky-header',
|
|
129
|
+
'StatusBar',
|
|
130
|
+
'Switch',
|
|
131
|
+
'Text',
|
|
132
|
+
'TextInput',
|
|
133
|
+
'TouchableHighlight',
|
|
134
|
+
'TouchableNativeFeedback',
|
|
135
|
+
'TouchableOpacity',
|
|
136
|
+
'TouchableWithoutFeedback',
|
|
137
|
+
'VirtualizedList',
|
|
138
|
+
'VirtualizedSectionList',
|
|
139
|
+
'symbiote-pressable',
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
// Inserting a bare raw-text node anywhere but inside a <Text> is invalid in Fabric (a
|
|
143
|
+
// stray RCTRawText would paint). Angular's ɵɵtext only ever lands text inside a <Text>,
|
|
144
|
+
// but guard anyway for parity with the Vue adapter and to fail loudly on a bad template.
|
|
145
|
+
function assertTextPlacement(child: ISymbioteNode, parent: IHostElement): void {
|
|
146
|
+
if (isRawText(child) && (isSurface(parent) || !parent.isText)) {
|
|
147
|
+
throw new Error(`Text string "${String(child.props.text)}" must be rendered inside a <Text>`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// One renderer per mounted surface. Every mutation asks the surface to (microtask-
|
|
152
|
+
// coalesced) recommit — the same seam Vue uses; a burst of Angular change-detection
|
|
153
|
+
// mutations collapses into one completeRoot. wolf-tui likewise reuses a single renderer
|
|
154
|
+
// instance for all components.
|
|
155
|
+
export class SymbioteRenderer implements Renderer2 {
|
|
156
|
+
readonly data: Record<string, unknown> = {};
|
|
157
|
+
// Angular calls destroyNode per-node only when this is non-null; teardown happens in
|
|
158
|
+
// render.ts (unmount), so per-node cleanup is a no-op.
|
|
159
|
+
destroyNode: ((node: ISymbioteNode) => void) | null = null;
|
|
160
|
+
|
|
161
|
+
constructor(private readonly surface: SymbioteSurface) {}
|
|
162
|
+
|
|
163
|
+
destroy(): void {}
|
|
164
|
+
|
|
165
|
+
createElement(name: string): IHostNode {
|
|
166
|
+
// `name` is the component's host tag — a symbiote intrinsic (`symbiote-view`,
|
|
167
|
+
// `symbiote-text`, …), a public ergonomic alias (`View`, `Text`), or a raw Fabric view
|
|
168
|
+
// name for a native leaf. Public aliases are normalized to their engine primitive name
|
|
169
|
+
// before descriptor lookup. descriptorFor resolves it; an unknown `symbiote-*` is a typo,
|
|
170
|
+
// any other string flows through as a raw Fabric name (events/processors derived from its
|
|
171
|
+
// ViewConfig). toPublicInstance grafts the imperative API (measure / setNativeProps /
|
|
172
|
+
// focus) onto the raw node in place, returning the SAME identity the commit mirror keys on.
|
|
173
|
+
const engineName = PRIMITIVE_SELECTOR_ALIAS[name] ?? name;
|
|
174
|
+
if (ANCHOR_HOST_COMPONENTS.has(engineName)) {
|
|
175
|
+
const anchor = tagAnchorForDebug(createAnchor());
|
|
176
|
+
dlog(`angular createElement ${name} -> anchor host ${describeHost(anchor)}`);
|
|
177
|
+
return anchor;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const descriptor = descriptorFor(engineName);
|
|
181
|
+
const node = createElement(descriptor.component, descriptor.isText);
|
|
182
|
+
dlog(`angular createElement ${name} -> ${descriptor.component}`);
|
|
183
|
+
return toPublicInstance(node);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
createComment(): IHostNode {
|
|
187
|
+
// Angular structural directives (*ngIf / @if / @for) need anchor nodes to track
|
|
188
|
+
// position. A real retained node the commit walk SKIPS — no native view. Twin of the
|
|
189
|
+
// Vue createComment path.
|
|
190
|
+
const anchor = tagAnchorForDebug(createAnchor());
|
|
191
|
+
dlog(`Angular renderer createComment -> ${describeHost(anchor)}`);
|
|
192
|
+
return anchor;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
createText(value: string): IHostNode {
|
|
196
|
+
return createRawText(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
appendChild(parent: IHostElement | null, newChild: IHostNode): void {
|
|
200
|
+
// Angular defers insertion for content awaiting its host component's own projection
|
|
201
|
+
// (see parentNode below) — mirrors Angular's own `if (parentRNode !== null)` guard in
|
|
202
|
+
// addLViewToLContainer: skip silently now, the later projection pass places it correctly.
|
|
203
|
+
if (parent === null) return;
|
|
204
|
+
assertTextPlacement(newChild, parent);
|
|
205
|
+
if (isSurface(parent)) {
|
|
206
|
+
dlog(`Angular renderer appendChild parent=surface child=${describeHost(newChild)}`);
|
|
207
|
+
parent.appendChild(newChild);
|
|
208
|
+
} else {
|
|
209
|
+
const projection = getScrollViewProjection(parent);
|
|
210
|
+
dlog(
|
|
211
|
+
`Angular renderer appendChild parent=${describeHost(parent)} child=${describeHost(newChild)} projection=${projection !== undefined}`,
|
|
212
|
+
);
|
|
213
|
+
if (projection !== undefined) {
|
|
214
|
+
projection.appendProjectedChild(parent, newChild, (target, child) =>
|
|
215
|
+
appendChild(target, child),
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
appendChild(parent, newChild);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.surface.requestCommit();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
insertBefore(parent: IHostElement | null, newChild: IHostNode, refChild: IHostNode | null): void {
|
|
225
|
+
if (parent === null) return; // see the appendChild guard above
|
|
226
|
+
assertTextPlacement(newChild, parent);
|
|
227
|
+
if (isSurface(parent)) {
|
|
228
|
+
dlog(
|
|
229
|
+
`Angular renderer insertBefore parent=surface child=${describeHost(newChild)} ref=${refChild ? describeHost(refChild) : 'null'}`,
|
|
230
|
+
);
|
|
231
|
+
if (refChild) parent.insertBefore(newChild, refChild);
|
|
232
|
+
else parent.appendChild(newChild);
|
|
233
|
+
} else {
|
|
234
|
+
const projection = getScrollViewProjection(parent);
|
|
235
|
+
dlog(
|
|
236
|
+
`Angular renderer insertBefore parent=${describeHost(parent)} child=${describeHost(newChild)} ref=${refChild ? describeHost(refChild) : 'null'} projection=${projection !== undefined}`,
|
|
237
|
+
);
|
|
238
|
+
if (projection !== undefined) {
|
|
239
|
+
projection.insertProjectedChild(parent, newChild, refChild, (target, child, before) => {
|
|
240
|
+
if (before === undefined) appendChild(target, child);
|
|
241
|
+
else insertBefore(target, child, before);
|
|
242
|
+
});
|
|
243
|
+
} else if (refChild) {
|
|
244
|
+
insertBefore(parent, newChild, refChild);
|
|
245
|
+
} else {
|
|
246
|
+
appendChild(parent, newChild);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
this.surface.requestCommit();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
removeChild(_parent: IHostElement | null, oldChild: IHostNode): void {
|
|
253
|
+
// Detach from the child's own retained parent (a top-level node lives in
|
|
254
|
+
// surface.children with no parent). Angular's `parent` arg is ignored in favor of the
|
|
255
|
+
// authoritative link, mirroring the Vue adapter's remove.
|
|
256
|
+
const angularParent = _parent !== null ? describeHost(_parent) : 'null';
|
|
257
|
+
const retainedParent = oldChild.parent !== undefined ? describeHost(oldChild.parent) : 'none';
|
|
258
|
+
const wasProjected = removeScrollViewProjectedChild(oldChild, (parent, child) =>
|
|
259
|
+
removeChild(parent, child),
|
|
260
|
+
);
|
|
261
|
+
dlog(
|
|
262
|
+
`Angular renderer removeChild angularParent=${angularParent} retainedParent=${retainedParent} child=${describeHost(oldChild)} viaProjection=${wasProjected}`,
|
|
263
|
+
);
|
|
264
|
+
if (!wasProjected) {
|
|
265
|
+
const parent = oldChild.parent;
|
|
266
|
+
if (parent !== undefined) removeChild(parent, oldChild);
|
|
267
|
+
else this.surface.removeChild(oldChild);
|
|
268
|
+
}
|
|
269
|
+
this.surface.requestCommit();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// FlatList/VirtualizedList cells are content projected into a component host (our
|
|
273
|
+
// ANCHOR_HOST_COMPONENTS, e.g. ScrollView) — Angular's own addLViewToLContainer
|
|
274
|
+
// (.vendors/angular node_manipulation.ts/container.ts) treats a null parent here as "defer —
|
|
275
|
+
// the child component's own <ng-content>/ɵɵprojection will place this once its structure
|
|
276
|
+
// resolves" (e.g. ScrollView's `@if(isHorizontal)` branch). Renderer2's contract types this
|
|
277
|
+
// return as nullable for exactly that reason; returning `this.surface` as a non-null fallback
|
|
278
|
+
// defeated that defer check and caused premature top-level insertion (2026-07: FlatList cells
|
|
279
|
+
// rendered outside their ScrollView).
|
|
280
|
+
//
|
|
281
|
+
// Safe only because appendChild/insertBefore above now also treat a null parent as "skip, wait
|
|
282
|
+
// for projection" — a second Angular call site (`insertAnchorNode`, hit whenever a directive
|
|
283
|
+
// does `inject(ViewContainerRef)`, e.g. VListOutletDirective) forwards this null straight into
|
|
284
|
+
// insertBefore without checking it; without that guard it crashed on-device.
|
|
285
|
+
parentNode(node: IHostNode): IHostElement | null {
|
|
286
|
+
return node.parent ?? null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
nextSibling(node: IHostNode): IHostNode | null {
|
|
290
|
+
const siblings = node.parent !== undefined ? node.parent.children : this.surface.children;
|
|
291
|
+
const index = siblings.indexOf(node);
|
|
292
|
+
return index >= 0 ? (siblings[index + 1] ?? null) : null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// locateHostElement always routes createComponent's `hostElement` THROUGH here as
|
|
296
|
+
// `selectorOrNode` (Angular's own core.mjs) — it is never bypassed just because a real
|
|
297
|
+
// object (vs. a selector string) was given. A string only reaches us on the (unused here)
|
|
298
|
+
// selector-string bootstrap path, so the surface is the fallback for that case only.
|
|
299
|
+
selectRootElement(selectorOrNode: string | IHostElement): IHostElement {
|
|
300
|
+
return typeof selectorOrNode === 'string' ? this.surface : selectorOrNode;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
setAttribute(el: IHostElement, name: string, value: string): void {
|
|
304
|
+
if (isSurface(el)) return;
|
|
305
|
+
routeProp(el, name, value);
|
|
306
|
+
this.surface.requestCommit();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
removeAttribute(el: IHostElement, name: string): void {
|
|
310
|
+
if (isSurface(el)) return;
|
|
311
|
+
routeProp(el, name, undefined);
|
|
312
|
+
this.surface.requestCommit();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Ivy compiles every class= / [class.foo] / [ngClass] form down to per-token addClass/
|
|
316
|
+
// removeClass calls (never a single setAttribute('class', ...) — confirmed against the
|
|
317
|
+
// vendored Ivy sources, see the symbiote-sfc-style-compiler skill), so a per-node token set
|
|
318
|
+
// is accumulated here and re-joined into one string on every change, then handed to
|
|
319
|
+
// routeProp('class', ...) exactly like Vue's template `class="..."` and React's JSX
|
|
320
|
+
// `className="..."` — all three resolve through the SAME centralized class+style merge in
|
|
321
|
+
// core/engine/src/node.ts, so a class registered via the SFC/CSS-Modules style compiler
|
|
322
|
+
// resolves identically regardless of adapter.
|
|
323
|
+
private readonly classTokens = new WeakMap<IHostNode, Set<string>>();
|
|
324
|
+
|
|
325
|
+
addClass(el: IHostElement, name: string): void {
|
|
326
|
+
if (isSurface(el)) return;
|
|
327
|
+
const tokens = this.classTokens.get(el) ?? new Set<string>();
|
|
328
|
+
tokens.add(name);
|
|
329
|
+
this.classTokens.set(el, tokens);
|
|
330
|
+
routeProp(el, 'class', [...tokens].join(' '));
|
|
331
|
+
this.surface.requestCommit();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
removeClass(el: IHostElement, name: string): void {
|
|
335
|
+
if (isSurface(el)) return;
|
|
336
|
+
const tokens = this.classTokens.get(el);
|
|
337
|
+
if (tokens === undefined) return;
|
|
338
|
+
tokens.delete(name);
|
|
339
|
+
routeProp(el, 'class', tokens.size > 0 ? [...tokens].join(' ') : undefined);
|
|
340
|
+
this.surface.requestCommit();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Angular decomposes a [style] binding into per-key setStyle calls (ɵɵstyleMap). RN wants
|
|
344
|
+
// the whole style object as one `style` prop, so merge each key into it — onto the explicit
|
|
345
|
+
// style half tracked by routeProp's centralized class+style merge (core/engine/src/node.ts),
|
|
346
|
+
// NOT el.props.style directly: that may now be the [classStyle, explicitStyle] array the
|
|
347
|
+
// merge writes, and spreading an array as a record would silently produce numeric-index keys.
|
|
348
|
+
setStyle(el: IHostElement, style: string, value: unknown): void {
|
|
349
|
+
if (isSurface(el)) return;
|
|
350
|
+
const current = getExplicitStyle(el);
|
|
351
|
+
const base = isRecord(current) ? current : {};
|
|
352
|
+
routeProp(el, 'style', { ...base, [style]: value });
|
|
353
|
+
this.surface.requestCommit();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
removeStyle(el: IHostElement, style: string): void {
|
|
357
|
+
if (isSurface(el)) return;
|
|
358
|
+
const current = getExplicitStyle(el);
|
|
359
|
+
if (!isRecord(current)) return;
|
|
360
|
+
const { [style]: _removed, ...rest } = current;
|
|
361
|
+
routeProp(el, 'style', rest);
|
|
362
|
+
this.surface.requestCommit();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// [prop]="x" bindings. routeProp makes the prop-vs-event decision from the node's
|
|
366
|
+
// ViewConfig (identical to React/Vue), so the whole flat-bag prop layer is shared.
|
|
367
|
+
setProperty(el: IHostElement, name: string, value: unknown): void {
|
|
368
|
+
if (isSurface(el)) return;
|
|
369
|
+
routeProp(el, name, value);
|
|
370
|
+
this.surface.requestCommit();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
setValue(node: IHostNode, value: string): void {
|
|
374
|
+
// A useful permanent seam: text mutations are low-frequency and the one place a stale
|
|
375
|
+
// `{{binding}}` (a change-detection gap) shows up as "the setValue never fired".
|
|
376
|
+
dlog(`Angular renderer setValue "${value}" on ${describeHost(node)}`);
|
|
377
|
+
setText(node, value);
|
|
378
|
+
this.surface.requestCommit();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// (event)="x" bindings. Angular hands the event name EXPLICITLY (no onX->x inference),
|
|
382
|
+
// so we drive the engine's structural event channel directly — the path setEventListener
|
|
383
|
+
// in core/engine/src/node.ts already names "Angular Renderer2.listen" for. Global targets
|
|
384
|
+
// (window/document/body) have no Fabric node, so they no-op.
|
|
385
|
+
listen(
|
|
386
|
+
target: unknown,
|
|
387
|
+
eventName: string,
|
|
388
|
+
callback: (event: unknown) => boolean | void,
|
|
389
|
+
): () => void {
|
|
390
|
+
if (!isSymbioteNode(target)) return () => {};
|
|
391
|
+
setEventListener(target, eventName, callback);
|
|
392
|
+
return () => setEventListener(target, eventName, undefined);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Provided to Angular as RendererFactory2; createRenderer returns the single
|
|
397
|
+
// surface-bound renderer for every component (begin/end commit-coalescing is unnecessary —
|
|
398
|
+
// requestCommit already microtask-coalesces).
|
|
399
|
+
export class SymbioteRendererFactory implements RendererFactory2 {
|
|
400
|
+
private renderer: SymbioteRenderer | undefined;
|
|
401
|
+
|
|
402
|
+
constructor(private readonly surface: SymbioteSurface) {}
|
|
403
|
+
|
|
404
|
+
createRenderer(_hostElement: unknown, _type: RendererType2 | null): Renderer2 {
|
|
405
|
+
return (this.renderer ??= new SymbioteRenderer(this.surface));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DestroyRef, inject, Injectable, signal } from '@angular/core';
|
|
2
|
+
import { Appearance, type IColorSchemeName } from '@symbiote-native/engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Angular twin of React's `useColorScheme` and Vue's `useColorScheme` composable. The service
|
|
6
|
+
* holds a Signal that tracks the device color scheme; consumers read it via `inject`:
|
|
7
|
+
*
|
|
8
|
+
* readonly colorScheme = inject(ColorSchemeService).colorScheme;
|
|
9
|
+
*
|
|
10
|
+
* State and lifecycle live in DI, not in a function closure.
|
|
11
|
+
*/
|
|
12
|
+
@Injectable({ providedIn: 'root' })
|
|
13
|
+
export class ColorSchemeService {
|
|
14
|
+
readonly colorScheme = signal<IColorSchemeName | null>(Appearance.getColorScheme());
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
const destroyRef = inject(DestroyRef);
|
|
18
|
+
|
|
19
|
+
const subscription = Appearance.addChangeListener(preferences => {
|
|
20
|
+
this.colorScheme.set(preferences.colorScheme);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
destroyRef.onDestroy(() => {
|
|
24
|
+
subscription.remove();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import '@angular/compiler';
|
|
2
|
+
import { Component, inject } from '@angular/core';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { Appearance, Dimensions } from '@symbiote-native/engine';
|
|
5
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
6
|
+
|
|
7
|
+
import { mount, unmount } from '../render';
|
|
8
|
+
import { ColorSchemeService } from './color-scheme.service';
|
|
9
|
+
import { WindowDimensionsService } from './window-dimensions.service';
|
|
10
|
+
|
|
11
|
+
const ROOT_TAG = 900;
|
|
12
|
+
const fabric = installFabric();
|
|
13
|
+
|
|
14
|
+
let capturedColorSchemeService: ColorSchemeService | undefined;
|
|
15
|
+
let capturedWindowDimensionsService: WindowDimensionsService | undefined;
|
|
16
|
+
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'symbiote-color-scheme-consumer',
|
|
19
|
+
standalone: true,
|
|
20
|
+
providers: [ColorSchemeService],
|
|
21
|
+
template: '',
|
|
22
|
+
})
|
|
23
|
+
class ColorSchemeConsumer {
|
|
24
|
+
readonly service = inject(ColorSchemeService);
|
|
25
|
+
constructor() {
|
|
26
|
+
capturedColorSchemeService = this.service;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Component({
|
|
31
|
+
selector: 'symbiote-window-dimensions-consumer',
|
|
32
|
+
standalone: true,
|
|
33
|
+
providers: [WindowDimensionsService],
|
|
34
|
+
template: '',
|
|
35
|
+
})
|
|
36
|
+
class WindowDimensionsConsumer {
|
|
37
|
+
readonly service = inject(WindowDimensionsService);
|
|
38
|
+
constructor() {
|
|
39
|
+
capturedWindowDimensionsService = this.service;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Component({
|
|
44
|
+
selector: 'symbiote-root-window-dimensions-consumer',
|
|
45
|
+
standalone: true,
|
|
46
|
+
template: '',
|
|
47
|
+
})
|
|
48
|
+
class RootWindowDimensionsConsumer {
|
|
49
|
+
readonly service = inject(WindowDimensionsService);
|
|
50
|
+
constructor() {
|
|
51
|
+
capturedWindowDimensionsService = this.service;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
capturedColorSchemeService = undefined;
|
|
57
|
+
capturedWindowDimensionsService = undefined;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
unmount(ROOT_TAG);
|
|
62
|
+
fabric.reset();
|
|
63
|
+
vi.restoreAllMocks();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('Angular DI services over engine modules', () => {
|
|
67
|
+
it('ColorSchemeService subscribes to Appearance and cleans up on unmount', async () => {
|
|
68
|
+
type IGenericListener = (...args: unknown[]) => void;
|
|
69
|
+
const activeListeners = new Set<IGenericListener>();
|
|
70
|
+
let emitToListeners: (preferences: { colorScheme: 'light' | 'dark' | null }) => void = () => {};
|
|
71
|
+
|
|
72
|
+
vi.spyOn(Appearance, 'getColorScheme').mockReturnValue('light');
|
|
73
|
+
vi.spyOn(Appearance, 'addChangeListener').mockImplementation(listener => {
|
|
74
|
+
activeListeners.add(listener as IGenericListener);
|
|
75
|
+
emitToListeners = preferences => {
|
|
76
|
+
for (const l of activeListeners) l(preferences);
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
remove: () => {
|
|
80
|
+
activeListeners.delete(listener as IGenericListener);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
mount(ROOT_TAG, ColorSchemeConsumer);
|
|
86
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
87
|
+
|
|
88
|
+
const service = capturedColorSchemeService;
|
|
89
|
+
if (!service) throw new Error('ColorSchemeService was not captured');
|
|
90
|
+
|
|
91
|
+
expect(service.colorScheme()).toBe('light');
|
|
92
|
+
expect(Appearance.addChangeListener).toHaveBeenCalledOnce();
|
|
93
|
+
|
|
94
|
+
emitToListeners({ colorScheme: 'dark' });
|
|
95
|
+
expect(service.colorScheme()).toBe('dark');
|
|
96
|
+
|
|
97
|
+
emitToListeners({ colorScheme: 'light' });
|
|
98
|
+
expect(service.colorScheme()).toBe('light');
|
|
99
|
+
|
|
100
|
+
unmount(ROOT_TAG);
|
|
101
|
+
expect(activeListeners.size).toBe(0);
|
|
102
|
+
|
|
103
|
+
// After unmount, the signal must stop reacting to further events.
|
|
104
|
+
emitToListeners({ colorScheme: 'dark' });
|
|
105
|
+
expect(service.colorScheme()).toBe('light');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('provides WindowDimensionsService from the Symbiote root injector', async () => {
|
|
109
|
+
const initialMetrics = { width: 100, height: 200, scale: 1, fontScale: 1 };
|
|
110
|
+
vi.spyOn(Dimensions, 'get').mockReturnValue(initialMetrics);
|
|
111
|
+
vi.spyOn(Dimensions, 'addEventListener').mockReturnValue({ remove: vi.fn() });
|
|
112
|
+
|
|
113
|
+
mount(ROOT_TAG, RootWindowDimensionsConsumer);
|
|
114
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
115
|
+
|
|
116
|
+
expect(capturedWindowDimensionsService?.dimensions()).toEqual(initialMetrics);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('WindowDimensionsService subscribes to Dimensions and ignores no-op updates', async () => {
|
|
120
|
+
const remove = vi.fn();
|
|
121
|
+
let capturedListener:
|
|
122
|
+
| ((set: {
|
|
123
|
+
window: { width: number; height: number; scale: number; fontScale: number };
|
|
124
|
+
screen: { width: number; height: number; scale: number; fontScale: number };
|
|
125
|
+
}) => void)
|
|
126
|
+
| undefined;
|
|
127
|
+
|
|
128
|
+
const initialMetrics = { width: 100, height: 200, scale: 1, fontScale: 1 };
|
|
129
|
+
vi.spyOn(Dimensions, 'get').mockReturnValue(initialMetrics);
|
|
130
|
+
vi.spyOn(Dimensions, 'addEventListener').mockImplementation((_type, listener) => {
|
|
131
|
+
capturedListener = set =>
|
|
132
|
+
listener(set as { window: typeof initialMetrics; screen: typeof initialMetrics });
|
|
133
|
+
return { remove };
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
mount(ROOT_TAG, WindowDimensionsConsumer);
|
|
137
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
138
|
+
|
|
139
|
+
const service = capturedWindowDimensionsService;
|
|
140
|
+
if (!service) throw new Error('WindowDimensionsService was not captured');
|
|
141
|
+
|
|
142
|
+
const initial = service.dimensions();
|
|
143
|
+
expect(initial).toEqual(initialMetrics);
|
|
144
|
+
expect(Dimensions.addEventListener).toHaveBeenCalledOnce();
|
|
145
|
+
|
|
146
|
+
// Same metrics: signal identity must be preserved (equality guard).
|
|
147
|
+
capturedListener?.({
|
|
148
|
+
window: { width: 100, height: 200, scale: 1, fontScale: 1 },
|
|
149
|
+
screen: { width: 100, height: 200, scale: 1, fontScale: 1 },
|
|
150
|
+
});
|
|
151
|
+
expect(service.dimensions()).toBe(initial);
|
|
152
|
+
|
|
153
|
+
// Different metrics: signal updates.
|
|
154
|
+
capturedListener?.({
|
|
155
|
+
window: { width: 300, height: 400, scale: 2, fontScale: 2 },
|
|
156
|
+
screen: { width: 300, height: 400, scale: 2, fontScale: 2 },
|
|
157
|
+
});
|
|
158
|
+
expect(service.dimensions()).toEqual({ width: 300, height: 400, scale: 2, fontScale: 2 });
|
|
159
|
+
|
|
160
|
+
unmount(ROOT_TAG);
|
|
161
|
+
expect(remove).toHaveBeenCalledOnce();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DestroyRef, inject, Injectable, signal } from '@angular/core';
|
|
2
|
+
import { Dimensions, type IDimensionsSet, type IDisplayMetrics } from '@symbiote-native/engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Angular twin of React's `useWindowDimensions` and Vue's `useWindowDimensions` composable. The
|
|
6
|
+
* service holds a Signal that tracks window metrics; consumers read it via `inject`:
|
|
7
|
+
*
|
|
8
|
+
* readonly dimensions = inject(WindowDimensionsService).dimensions;
|
|
9
|
+
*
|
|
10
|
+
* State and lifecycle live in DI, not in a function closure.
|
|
11
|
+
*/
|
|
12
|
+
@Injectable({ providedIn: 'root' })
|
|
13
|
+
export class WindowDimensionsService {
|
|
14
|
+
readonly dimensions = signal<IDisplayMetrics>(Dimensions.get('window'));
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
const destroyRef = inject(DestroyRef);
|
|
18
|
+
|
|
19
|
+
const handleChange = (window: IDisplayMetrics): void => {
|
|
20
|
+
const current = this.dimensions();
|
|
21
|
+
if (
|
|
22
|
+
current.width !== window.width ||
|
|
23
|
+
current.height !== window.height ||
|
|
24
|
+
current.scale !== window.scale ||
|
|
25
|
+
current.fontScale !== window.fontScale
|
|
26
|
+
) {
|
|
27
|
+
this.dimensions.set(window);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const subscription = Dimensions.addEventListener('change', (set: IDimensionsSet) => {
|
|
32
|
+
handleChange(set.window);
|
|
33
|
+
});
|
|
34
|
+
// Re-check once after subscribing to close the gap between construction and the listener.
|
|
35
|
+
handleChange(Dimensions.get('window'));
|
|
36
|
+
|
|
37
|
+
destroyRef.onDestroy(() => {
|
|
38
|
+
subscription.remove();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|