@nativescript/vite 8.0.0-alpha.11 → 8.0.0-alpha.13
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/configuration/angular.d.ts +34 -1
- package/configuration/angular.js +177 -10
- package/configuration/angular.js.map +1 -1
- package/helpers/angular/inject-hmr-vite-ignore.d.ts +75 -0
- package/helpers/angular/inject-hmr-vite-ignore.js +288 -0
- package/helpers/angular/inject-hmr-vite-ignore.js.map +1 -0
- package/helpers/resolver.js +9 -1
- package/helpers/resolver.js.map +1 -1
- package/hmr/client/index.js +98 -0
- package/hmr/client/index.js.map +1 -1
- package/hmr/frameworks/angular/client/index.js +16 -16
- package/hmr/frameworks/angular/client/index.js.map +1 -1
- package/hmr/server/websocket-angular-hot-update.js +3 -3
- package/hmr/server/websocket-angular-hot-update.js.map +1 -1
- package/hmr/server/websocket-core-bridge.js +12 -1
- package/hmr/server/websocket-core-bridge.js.map +1 -1
- package/hmr/server/websocket.js +314 -20
- package/hmr/server/websocket.js.map +1 -1
- package/hmr/shared/runtime/dev-overlay.d.ts +30 -2
- package/hmr/shared/runtime/dev-overlay.js +443 -38
- package/hmr/shared/runtime/dev-overlay.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type HmrOverlayTone = 'info' | 'warn' | 'error' | 'success';
|
|
2
2
|
type HmrOverlayMode = 'hidden' | 'boot' | 'connection' | 'update';
|
|
3
|
+
export type HmrOverlayPosition = 'top' | 'bottom' | 'center';
|
|
3
4
|
export type HmrBootStage = 'placeholder' | 'probing-origin' | 'loading-entry-runtime' | 'configuring-import-map' | 'loading-runtime-bridge' | 'loading-core-bridge' | 'preloading-style-scope' | 'installing-css' | 'importing-main' | 'waiting-for-app' | 'app-root-committed' | 'ready' | 'error';
|
|
4
5
|
export type HmrConnectionStage = 'connecting' | 'reconnecting' | 'synchronizing' | 'offline' | 'healthy';
|
|
5
6
|
export type HmrUpdateStage = 'received' | 'evicting' | 'reimporting' | 'rebooting' | 'complete';
|
|
@@ -30,6 +31,22 @@ type HmrOverlayApi = {
|
|
|
30
31
|
hide: (reason?: string) => void;
|
|
31
32
|
getSnapshot: () => HmrOverlaySnapshot;
|
|
32
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the configured live-overlay position.
|
|
36
|
+
*
|
|
37
|
+
* Reads `globalThis.__NS_HMR_OVERLAY_POSITION__` so a project can
|
|
38
|
+
* override the default at boot time (e.g. inside `app.ts` before the
|
|
39
|
+
* Vite session bootstraps). Falls back to 'top' which gives the
|
|
40
|
+
* toast-style chip with a slide-in animation and safe-area padding.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getHmrDevOverlayPosition(): HmrOverlayPosition;
|
|
43
|
+
/**
|
|
44
|
+
* Imperative setter for the live-overlay position. Re-applies the
|
|
45
|
+
* current snapshot so the change is visible without waiting for the
|
|
46
|
+
* next HMR cycle. Useful during dev to A/B between top/bottom/center
|
|
47
|
+
* without restarting the app.
|
|
48
|
+
*/
|
|
49
|
+
export declare function setHmrDevOverlayPosition(position: HmrOverlayPosition): void;
|
|
33
50
|
export declare function createBootOverlaySnapshot(stage: HmrBootStage, info?: HmrOverlayStageInfo): HmrOverlaySnapshot;
|
|
34
51
|
export declare function createConnectionOverlaySnapshot(stage: HmrConnectionStage, info?: HmrOverlayStageInfo): HmrOverlaySnapshot;
|
|
35
52
|
export declare function createUpdateOverlaySnapshot(stage: HmrUpdateStage, info?: HmrOverlayStageInfo): HmrOverlaySnapshot;
|
|
@@ -61,8 +78,17 @@ export type IosOverlayLayout = {
|
|
|
61
78
|
/**
|
|
62
79
|
* Layout math for the live overlay when it runs inside its own UIWindow.
|
|
63
80
|
* Pure, deterministic and independent of UIKit so we can verify the rules
|
|
64
|
-
* (max panel width,
|
|
65
|
-
* from tests.
|
|
81
|
+
* (max panel width, position-aware placement, safe-area clamping, sane
|
|
82
|
+
* defaults) from tests.
|
|
83
|
+
*
|
|
84
|
+
* `position` controls where the panel sits vertically:
|
|
85
|
+
* - 'top': hugs `safeInsets.top + toastVerticalInset` so the chip
|
|
86
|
+
* sits just below the notch / Dynamic Island.
|
|
87
|
+
* - 'bottom': hugs `viewHeight - safeInsets.bottom - panelHeight -
|
|
88
|
+
* toastVerticalInset` so the chip sits just above the
|
|
89
|
+
* home indicator / nav bar.
|
|
90
|
+
* - 'center': original modal placement (vertically centered, clamped
|
|
91
|
+
* so it never crosses the top safe-area inset).
|
|
66
92
|
*/
|
|
67
93
|
export declare function computeIosOverlayLayout(input: {
|
|
68
94
|
viewWidth: number;
|
|
@@ -75,6 +101,8 @@ export declare function computeIosOverlayLayout(input: {
|
|
|
75
101
|
panelPadding?: number;
|
|
76
102
|
interLabelSpacing?: number;
|
|
77
103
|
minTopInset?: number;
|
|
104
|
+
position?: HmrOverlayPosition;
|
|
105
|
+
toastVerticalInset?: number;
|
|
78
106
|
}): IosOverlayLayout;
|
|
79
107
|
export declare function ensureHmrDevOverlayRuntimeInstalled(verbose?: boolean): HmrOverlayApi;
|
|
80
108
|
export declare function createHmrBootOverlayPage(verbose?: boolean): any | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const DEFAULT_OVERLAY_POSITION = 'top';
|
|
1
2
|
const BOOT_TITLE = 'NativeScript Vite preparing dev session...';
|
|
2
3
|
const DEFAULT_SNAPSHOT = {
|
|
3
4
|
visible: false,
|
|
@@ -14,6 +15,37 @@ const DEFAULT_SNAPSHOT = {
|
|
|
14
15
|
function getOverlayGlobal() {
|
|
15
16
|
return globalThis;
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the configured live-overlay position.
|
|
20
|
+
*
|
|
21
|
+
* Reads `globalThis.__NS_HMR_OVERLAY_POSITION__` so a project can
|
|
22
|
+
* override the default at boot time (e.g. inside `app.ts` before the
|
|
23
|
+
* Vite session bootstraps). Falls back to 'top' which gives the
|
|
24
|
+
* toast-style chip with a slide-in animation and safe-area padding.
|
|
25
|
+
*/
|
|
26
|
+
export function getHmrDevOverlayPosition() {
|
|
27
|
+
const g = getOverlayGlobal();
|
|
28
|
+
const stored = g.__NS_HMR_OVERLAY_POSITION__;
|
|
29
|
+
if (stored === 'top' || stored === 'bottom' || stored === 'center') {
|
|
30
|
+
return stored;
|
|
31
|
+
}
|
|
32
|
+
return DEFAULT_OVERLAY_POSITION;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Imperative setter for the live-overlay position. Re-applies the
|
|
36
|
+
* current snapshot so the change is visible without waiting for the
|
|
37
|
+
* next HMR cycle. Useful during dev to A/B between top/bottom/center
|
|
38
|
+
* without restarting the app.
|
|
39
|
+
*/
|
|
40
|
+
export function setHmrDevOverlayPosition(position) {
|
|
41
|
+
if (position !== 'top' && position !== 'bottom' && position !== 'center') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const g = getOverlayGlobal();
|
|
45
|
+
g.__NS_HMR_OVERLAY_POSITION__ = position;
|
|
46
|
+
const state = getRuntimeState();
|
|
47
|
+
applyRuntimeSnapshot(state.snapshot);
|
|
48
|
+
}
|
|
17
49
|
function getRuntimeState() {
|
|
18
50
|
const g = getOverlayGlobal();
|
|
19
51
|
if (!g.__NS_HMR_DEV_OVERLAY_STATE__) {
|
|
@@ -603,8 +635,18 @@ function buildLiveOverlayView(snapshot) {
|
|
|
603
635
|
overlay.height = '100%';
|
|
604
636
|
overlay.horizontalAlignment = 'stretch';
|
|
605
637
|
overlay.verticalAlignment = 'stretch';
|
|
638
|
+
// Toast mode lets touches reach the underlying app. We flip
|
|
639
|
+
// isUserInteractionEnabled in applySnapshotToLiveRefs based on
|
|
640
|
+
// the resolved position, but keep it false here as a safe default
|
|
641
|
+
// (the panel itself is purely informational).
|
|
642
|
+
try {
|
|
643
|
+
overlay.isUserInteractionEnabled = false;
|
|
644
|
+
}
|
|
645
|
+
catch { }
|
|
606
646
|
const panel = new StackLayout();
|
|
607
647
|
panel.horizontalAlignment = 'center';
|
|
648
|
+
// Vertical alignment is overridden in applySnapshotToLiveRefs
|
|
649
|
+
// based on getHmrDevOverlayPosition(); 'middle' is the default
|
|
608
650
|
panel.verticalAlignment = 'middle';
|
|
609
651
|
panel.width = 320;
|
|
610
652
|
panel.margin = 24;
|
|
@@ -626,6 +668,8 @@ function buildLiveOverlayView(snapshot) {
|
|
|
626
668
|
overlay,
|
|
627
669
|
titleLabel,
|
|
628
670
|
statusLabel,
|
|
671
|
+
wasVisible: false,
|
|
672
|
+
currentPosition: getHmrDevOverlayPosition(),
|
|
629
673
|
};
|
|
630
674
|
applySnapshotToLiveRefs(refs, snapshot);
|
|
631
675
|
return refs;
|
|
@@ -683,32 +727,42 @@ function applySnapshotToLiveRefs(refs, snapshot) {
|
|
|
683
727
|
return;
|
|
684
728
|
}
|
|
685
729
|
// 'update' mode shares the live (in-tree) overlay chrome with
|
|
686
|
-
// 'connection'. Both render a
|
|
687
|
-
//
|
|
730
|
+
// 'connection'. Both render a small panel inside the page; only
|
|
731
|
+
// the colours, text, and (now) panel position change with the
|
|
732
|
+
// snapshot's tone and the configured overlay position.
|
|
688
733
|
const visible = snapshot.visible && (snapshot.mode === 'connection' || snapshot.mode === 'update');
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
// subtle on light app backgrounds that users couldn't
|
|
694
|
-
// tell the overlay had even fired. 50% alpha keeps the
|
|
695
|
-
// underlying app legible while making the apply event
|
|
696
|
-
// unmistakable during the (often <300ms) cycle.
|
|
697
|
-
// default → warm orange (existing connection-overlay look)
|
|
698
|
-
const overlayBg = snapshot.tone === 'error' ? '#b4181068' : snapshot.tone === 'success' ? '#1f883d80' : '#a1771683';
|
|
699
|
-
refs.overlay.backgroundColor = asColor(overlayBg);
|
|
734
|
+
const wasVisible = !!refs.wasVisible;
|
|
735
|
+
const position = getHmrDevOverlayPosition();
|
|
736
|
+
const previousPosition = refs.currentPosition || position;
|
|
737
|
+
const isToast = position !== 'center';
|
|
700
738
|
refs.titleLabel.text = snapshot.title;
|
|
739
|
+
refs.statusLabel.text = formatStatusText(snapshot);
|
|
701
740
|
const textColor = snapshot.tone === 'error' ? '#b41810e6' : snapshot.tone === 'success' ? '#0e6e2fff' : '#563e3fb1';
|
|
702
741
|
refs.titleLabel.color = asColor(textColor);
|
|
703
|
-
refs.statusLabel.text = formatStatusText(snapshot);
|
|
704
742
|
refs.statusLabel.color = asColor(textColor);
|
|
743
|
+
// Backdrop tints (centered modal only). Toast modes use a fully
|
|
744
|
+
// transparent backdrop so the rest of the app stays visible AND
|
|
745
|
+
// reachable; the panel itself carries enough colour to stand out.
|
|
746
|
+
if (isToast) {
|
|
747
|
+
refs.overlay.backgroundColor = asColor('transparent');
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
// Original wash-by-tone for centered:
|
|
751
|
+
// error → red wash (matches existing UX)
|
|
752
|
+
// success → richer green wash so the apply event is visible
|
|
753
|
+
// on bright app backgrounds
|
|
754
|
+
// default → warm orange (existing connection-overlay look)
|
|
755
|
+
const overlayBg = snapshot.tone === 'error' ? '#b4181068' : snapshot.tone === 'success' ? '#1f883d80' : '#a1771683';
|
|
756
|
+
refs.overlay.backgroundColor = asColor(overlayBg);
|
|
757
|
+
}
|
|
758
|
+
// Panel chrome — toast and centered share the same chip look,
|
|
759
|
+
// just position differs. We keep the slightly richer green tint
|
|
760
|
+
// for the HMR success state so it pops without needing the
|
|
761
|
+
// backdrop wash.
|
|
762
|
+
let panel = null;
|
|
705
763
|
try {
|
|
706
|
-
|
|
764
|
+
panel = refs.titleLabel.parent;
|
|
707
765
|
if (panel) {
|
|
708
|
-
// Slightly richer green-tinted panel for HMR-apply so the
|
|
709
|
-
// title/status text reads at a glance against the brighter
|
|
710
|
-
// backdrop wash. White panel for connection/error keeps
|
|
711
|
-
// existing UX intact.
|
|
712
766
|
const panelBg = snapshot.tone === 'success' ? '#E6F8E9FF' : '#FFFFFFFF';
|
|
713
767
|
panel.backgroundColor = asColor(panelBg);
|
|
714
768
|
panel.opacity = 1;
|
|
@@ -717,9 +771,142 @@ function applySnapshotToLiveRefs(refs, snapshot) {
|
|
|
717
771
|
panel.borderRadius = 12;
|
|
718
772
|
}
|
|
719
773
|
catch { }
|
|
774
|
+
// Position-aware alignment. The wrapper GridLayout fills
|
|
775
|
+
// the page content area, which on iOS is already inside
|
|
776
|
+
// the safe area; we add a small extra margin so the chip
|
|
777
|
+
// doesn't kiss the notch / home indicator.
|
|
778
|
+
try {
|
|
779
|
+
if (position === 'top') {
|
|
780
|
+
panel.verticalAlignment = 'top';
|
|
781
|
+
panel.margin = '12 16 0 16';
|
|
782
|
+
}
|
|
783
|
+
else if (position === 'bottom') {
|
|
784
|
+
panel.verticalAlignment = 'bottom';
|
|
785
|
+
panel.margin = '0 16 12 16';
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
panel.verticalAlignment = 'middle';
|
|
789
|
+
panel.margin = 24;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
catch { }
|
|
720
793
|
}
|
|
721
794
|
}
|
|
722
795
|
catch { }
|
|
796
|
+
// Touch passthrough for toast; centered mode keeps the
|
|
797
|
+
// blocking modal so the dim backdrop is meaningful.
|
|
798
|
+
try {
|
|
799
|
+
refs.overlay.isUserInteractionEnabled = !isToast;
|
|
800
|
+
}
|
|
801
|
+
catch { }
|
|
802
|
+
const positionChanged = previousPosition !== position;
|
|
803
|
+
const justAppeared = visible && (!wasVisible || positionChanged);
|
|
804
|
+
const justDismissed = !visible && wasVisible;
|
|
805
|
+
if (justAppeared) {
|
|
806
|
+
refs.overlay.visibility = 'visible';
|
|
807
|
+
if (isToast && panel && typeof panel.animate === 'function') {
|
|
808
|
+
animateLivePanelIn(panel, position);
|
|
809
|
+
}
|
|
810
|
+
else if (panel) {
|
|
811
|
+
try {
|
|
812
|
+
panel.translateY = 0;
|
|
813
|
+
panel.opacity = 1;
|
|
814
|
+
}
|
|
815
|
+
catch { }
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else if (justDismissed) {
|
|
819
|
+
if (isToast && panel && typeof panel.animate === 'function') {
|
|
820
|
+
animateLivePanelOut(panel, previousPosition, () => {
|
|
821
|
+
try {
|
|
822
|
+
refs.overlay.visibility = 'collapse';
|
|
823
|
+
}
|
|
824
|
+
catch { }
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
refs.overlay.visibility = 'collapse';
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
refs.overlay.visibility = visible ? 'visible' : 'collapse';
|
|
833
|
+
}
|
|
834
|
+
if (typeof refs.wasVisible !== 'undefined')
|
|
835
|
+
refs.wasVisible = visible;
|
|
836
|
+
if (typeof refs.currentPosition !== 'undefined')
|
|
837
|
+
refs.currentPosition = position;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Slide-in animation for the in-tree toast panel.
|
|
841
|
+
*
|
|
842
|
+
* NativeScript's `View.animate({ translate, opacity, duration, curve })`
|
|
843
|
+
* is widely available across Core versions, so we don't depend on any
|
|
844
|
+
* specific curve enum being importable here. We use a moderate-to-snappy
|
|
845
|
+
* 320ms ease-out which feels close to a UIView spring without needing
|
|
846
|
+
* platform-specific APIs.
|
|
847
|
+
*/
|
|
848
|
+
function animateLivePanelIn(panel, position) {
|
|
849
|
+
if (!panel || typeof panel.animate !== 'function')
|
|
850
|
+
return;
|
|
851
|
+
try {
|
|
852
|
+
const startY = position === 'bottom' ? 80 : -80;
|
|
853
|
+
panel.translateY = startY;
|
|
854
|
+
panel.opacity = 0;
|
|
855
|
+
const result = panel.animate({
|
|
856
|
+
translate: { x: 0, y: 0 },
|
|
857
|
+
opacity: 1,
|
|
858
|
+
duration: 320,
|
|
859
|
+
curve: 'easeOut',
|
|
860
|
+
});
|
|
861
|
+
if (result && typeof result.catch === 'function') {
|
|
862
|
+
result.catch(() => {
|
|
863
|
+
try {
|
|
864
|
+
panel.translateY = 0;
|
|
865
|
+
panel.opacity = 1;
|
|
866
|
+
}
|
|
867
|
+
catch { }
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
catch {
|
|
872
|
+
try {
|
|
873
|
+
panel.translateY = 0;
|
|
874
|
+
panel.opacity = 1;
|
|
875
|
+
}
|
|
876
|
+
catch { }
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function animateLivePanelOut(panel, position, onComplete) {
|
|
880
|
+
if (!panel || typeof panel.animate !== 'function') {
|
|
881
|
+
onComplete();
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
try {
|
|
885
|
+
const targetY = position === 'bottom' ? 80 : -80;
|
|
886
|
+
const result = panel.animate({
|
|
887
|
+
translate: { x: 0, y: targetY },
|
|
888
|
+
opacity: 0,
|
|
889
|
+
duration: 220,
|
|
890
|
+
curve: 'easeIn',
|
|
891
|
+
});
|
|
892
|
+
const finish = () => {
|
|
893
|
+
try {
|
|
894
|
+
panel.translateY = 0;
|
|
895
|
+
panel.opacity = 1;
|
|
896
|
+
}
|
|
897
|
+
catch { }
|
|
898
|
+
onComplete();
|
|
899
|
+
};
|
|
900
|
+
if (result && typeof result.then === 'function') {
|
|
901
|
+
result.then(finish, finish);
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
finish();
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
onComplete();
|
|
909
|
+
}
|
|
723
910
|
}
|
|
724
911
|
// pure helpers for iOS window promotion. Factored out so the layout
|
|
725
912
|
// math and window-level selection stay unit-testable without booting a
|
|
@@ -739,8 +926,17 @@ export function computeIosOverlayWindowLevel(baseAlert) {
|
|
|
739
926
|
/**
|
|
740
927
|
* Layout math for the live overlay when it runs inside its own UIWindow.
|
|
741
928
|
* Pure, deterministic and independent of UIKit so we can verify the rules
|
|
742
|
-
* (max panel width,
|
|
743
|
-
* from tests.
|
|
929
|
+
* (max panel width, position-aware placement, safe-area clamping, sane
|
|
930
|
+
* defaults) from tests.
|
|
931
|
+
*
|
|
932
|
+
* `position` controls where the panel sits vertically:
|
|
933
|
+
* - 'top': hugs `safeInsets.top + toastVerticalInset` so the chip
|
|
934
|
+
* sits just below the notch / Dynamic Island.
|
|
935
|
+
* - 'bottom': hugs `viewHeight - safeInsets.bottom - panelHeight -
|
|
936
|
+
* toastVerticalInset` so the chip sits just above the
|
|
937
|
+
* home indicator / nav bar.
|
|
938
|
+
* - 'center': original modal placement (vertically centered, clamped
|
|
939
|
+
* so it never crosses the top safe-area inset).
|
|
744
940
|
*/
|
|
745
941
|
export function computeIosOverlayLayout(input) {
|
|
746
942
|
const viewWidth = Math.max(0, Number(input.viewWidth) || 0);
|
|
@@ -758,15 +954,41 @@ export function computeIosOverlayLayout(input) {
|
|
|
758
954
|
const panelPadding = Math.max(0, Number(input.panelPadding ?? 16));
|
|
759
955
|
const interLabelSpacing = Math.max(0, Number(input.interLabelSpacing ?? 10));
|
|
760
956
|
const minTopInset = Math.max(0, Number(input.minTopInset ?? 20));
|
|
957
|
+
// Default to 'center' on the pure function so the existing
|
|
958
|
+
// snapshot/layout tests remain stable; the runtime call site
|
|
959
|
+
// (layoutIosOverlayRefs) reads the configured position from
|
|
960
|
+
// `getHmrDevOverlayPosition()` and forwards it explicitly.
|
|
961
|
+
const position = input.position ?? 'center';
|
|
962
|
+
// Distance between the panel and the safe-area edge in toast
|
|
963
|
+
// modes. 8pt mirrors the typical iOS notification chip inset and
|
|
964
|
+
// keeps the chip from hugging the notch / home indicator.
|
|
965
|
+
const toastVerticalInset = Math.max(0, Number(input.toastVerticalInset ?? 8));
|
|
761
966
|
const available = Math.max(0, viewWidth - 2 * horizontalMargin - safeInsets.left - safeInsets.right);
|
|
762
967
|
const panelWidth = Math.min(maxPanelWidth, available);
|
|
763
968
|
const innerWidth = Math.max(0, panelWidth - 2 * panelPadding);
|
|
764
969
|
const spacing = titleHeight > 0 && statusHeight > 0 ? interLabelSpacing : 0;
|
|
765
970
|
const panelHeight = panelPadding * 2 + titleHeight + spacing + statusHeight;
|
|
766
971
|
const panelX = Math.max(0, (viewWidth - panelWidth) / 2);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
972
|
+
let panelY;
|
|
973
|
+
if (position === 'top') {
|
|
974
|
+
// Pin to the top safe-area inset (just below notch / Dynamic
|
|
975
|
+
// Island). Clamp non-negative for fully-NaN input.
|
|
976
|
+
panelY = Math.max(0, safeInsets.top + toastVerticalInset);
|
|
977
|
+
}
|
|
978
|
+
else if (position === 'bottom') {
|
|
979
|
+
// Pin to the bottom safe-area inset (just above home indicator
|
|
980
|
+
// / nav bar). If the panel can't fit between the safe-area
|
|
981
|
+
// insets we fall back to the top safe-area edge so the chip is
|
|
982
|
+
// always visible (rather than getting clipped off-screen).
|
|
983
|
+
const desired = viewHeight - safeInsets.bottom - panelHeight - toastVerticalInset;
|
|
984
|
+
panelY = Math.max(safeInsets.top + minTopInset, desired);
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
// Center vertically, but never cross the top safe-area inset
|
|
988
|
+
// (notch/Dynamic Island). Original modal placement.
|
|
989
|
+
const centered = (viewHeight - panelHeight) / 2;
|
|
990
|
+
panelY = Math.max(safeInsets.top + minTopInset, centered);
|
|
991
|
+
}
|
|
770
992
|
return {
|
|
771
993
|
backdrop: { x: 0, y: 0, width: viewWidth, height: viewHeight },
|
|
772
994
|
panel: { x: panelX, y: panelY, width: panelWidth, height: panelHeight },
|
|
@@ -880,7 +1102,32 @@ function buildIosOverlayRefs(state) {
|
|
|
880
1102
|
statusLabel.font = UIFont.systemFontOfSize(13);
|
|
881
1103
|
statusLabel.textColor = UIColor.darkGrayColor;
|
|
882
1104
|
panel.addSubview(statusLabel);
|
|
883
|
-
|
|
1105
|
+
// Subtle drop-shadow so the toast chip reads against light app
|
|
1106
|
+
// content (white-on-white is invisible). The error / centered
|
|
1107
|
+
// branches still get the dim backdrop, so the shadow is mostly
|
|
1108
|
+
// a no-op for them — but it's a one-time setup.
|
|
1109
|
+
try {
|
|
1110
|
+
panel.layer.shadowColor = UIColor.blackColor.CGColor;
|
|
1111
|
+
panel.layer.shadowOpacity = 0.18;
|
|
1112
|
+
panel.layer.shadowRadius = 8;
|
|
1113
|
+
panel.layer.shadowOffset = { width: 0, height: 2 };
|
|
1114
|
+
panel.layer.masksToBounds = false;
|
|
1115
|
+
}
|
|
1116
|
+
catch { }
|
|
1117
|
+
// `wasVisible` / `currentPosition` are mutated by
|
|
1118
|
+
// applySnapshotToIosRefs when the snapshot triggers a slide-in
|
|
1119
|
+
// or slide-out. They start in the "hidden" state so the very
|
|
1120
|
+
// first visible snapshot animates in cleanly.
|
|
1121
|
+
return {
|
|
1122
|
+
window,
|
|
1123
|
+
controller,
|
|
1124
|
+
backdrop,
|
|
1125
|
+
panel,
|
|
1126
|
+
titleLabel,
|
|
1127
|
+
statusLabel,
|
|
1128
|
+
wasVisible: false,
|
|
1129
|
+
currentPosition: getHmrDevOverlayPosition(),
|
|
1130
|
+
};
|
|
884
1131
|
}
|
|
885
1132
|
catch (err) {
|
|
886
1133
|
console.warn('[ns-hmr-overlay] iOS overlay construction failed:', err?.message || err);
|
|
@@ -903,7 +1150,7 @@ function ensureIosOverlayRefs(state) {
|
|
|
903
1150
|
}
|
|
904
1151
|
return state.iosRefs;
|
|
905
1152
|
}
|
|
906
|
-
function layoutIosOverlayRefs(refs) {
|
|
1153
|
+
function layoutIosOverlayRefs(refs, position) {
|
|
907
1154
|
try {
|
|
908
1155
|
const bounds = refs.controller.view.bounds;
|
|
909
1156
|
const viewWidth = Number(bounds?.size?.width) || 0;
|
|
@@ -934,6 +1181,7 @@ function layoutIosOverlayRefs(refs) {
|
|
|
934
1181
|
maxPanelWidth,
|
|
935
1182
|
horizontalMargin,
|
|
936
1183
|
panelPadding,
|
|
1184
|
+
position,
|
|
937
1185
|
});
|
|
938
1186
|
const toCgRect = (rect) => ({
|
|
939
1187
|
origin: { x: rect.x, y: rect.y },
|
|
@@ -943,9 +1191,112 @@ function layoutIosOverlayRefs(refs) {
|
|
|
943
1191
|
refs.panel.frame = toCgRect(layout.panel);
|
|
944
1192
|
refs.titleLabel.frame = toCgRect(layout.title);
|
|
945
1193
|
refs.statusLabel.frame = toCgRect(layout.status);
|
|
1194
|
+
return layout;
|
|
946
1195
|
}
|
|
947
1196
|
catch (err) {
|
|
948
1197
|
console.warn('[ns-hmr-overlay] iOS overlay layout failed:', err?.message || err);
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Slide-in animation for the iOS toast panel. Off-screen start frame
|
|
1203
|
+
* lives just above (top) or below (bottom) the visible area; the panel
|
|
1204
|
+
* snaps to its target frame with a spring so the motion feels physical
|
|
1205
|
+
* without the heavy "settle" overshoot of a hard spring (damping 0.85
|
|
1206
|
+
* lands quickly with a small overshoot).
|
|
1207
|
+
*/
|
|
1208
|
+
function animateIosPanelIn(refs, position, layout) {
|
|
1209
|
+
const g = getOverlayGlobal();
|
|
1210
|
+
const UIView = g?.UIView;
|
|
1211
|
+
if (!UIView)
|
|
1212
|
+
return;
|
|
1213
|
+
try {
|
|
1214
|
+
const targetFrame = {
|
|
1215
|
+
origin: { x: layout.panel.x, y: layout.panel.y },
|
|
1216
|
+
size: { width: layout.panel.width, height: layout.panel.height },
|
|
1217
|
+
};
|
|
1218
|
+
// Off-screen start: distance includes a small fudge so the
|
|
1219
|
+
// shadow blur tail isn't visible at t=0.
|
|
1220
|
+
const startY = position === 'bottom' ? layout.backdrop.height + 24 : -(layout.panel.height + 24);
|
|
1221
|
+
refs.panel.frame = {
|
|
1222
|
+
origin: { x: layout.panel.x, y: startY },
|
|
1223
|
+
size: { width: layout.panel.width, height: layout.panel.height },
|
|
1224
|
+
};
|
|
1225
|
+
refs.panel.alpha = 0;
|
|
1226
|
+
try {
|
|
1227
|
+
if (typeof UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion === 'function') {
|
|
1228
|
+
UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(0.42, 0, 0.85, 0.7, 0, () => {
|
|
1229
|
+
refs.panel.frame = targetFrame;
|
|
1230
|
+
refs.panel.alpha = 1;
|
|
1231
|
+
}, null);
|
|
1232
|
+
}
|
|
1233
|
+
else if (typeof UIView.animateWithDurationAnimations === 'function') {
|
|
1234
|
+
UIView.animateWithDurationAnimations(0.32, () => {
|
|
1235
|
+
refs.panel.frame = targetFrame;
|
|
1236
|
+
refs.panel.alpha = 1;
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
refs.panel.frame = targetFrame;
|
|
1241
|
+
refs.panel.alpha = 1;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
catch {
|
|
1245
|
+
refs.panel.frame = targetFrame;
|
|
1246
|
+
refs.panel.alpha = 1;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
catch { }
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Slide-out animation for the iOS toast panel. Mirrors animateIosPanelIn:
|
|
1253
|
+
* the panel travels to the nearest off-screen edge while fading out so
|
|
1254
|
+
* the dismissal still feels intentional even on fast HMR cycles.
|
|
1255
|
+
*/
|
|
1256
|
+
function animateIosPanelOut(refs, position, onComplete) {
|
|
1257
|
+
const g = getOverlayGlobal();
|
|
1258
|
+
const UIView = g?.UIView;
|
|
1259
|
+
const currentFrame = refs.panel?.frame;
|
|
1260
|
+
if (!UIView || !currentFrame) {
|
|
1261
|
+
onComplete();
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
try {
|
|
1265
|
+
const bounds = refs.controller?.view?.bounds;
|
|
1266
|
+
const viewHeight = Number(bounds?.size?.height) || 0;
|
|
1267
|
+
const targetY = position === 'bottom' ? viewHeight + 24 : -(Number(currentFrame.size?.height) + 24);
|
|
1268
|
+
const startFrame = currentFrame;
|
|
1269
|
+
const targetFrame = {
|
|
1270
|
+
origin: { x: Number(startFrame.origin?.x) || 0, y: targetY },
|
|
1271
|
+
size: startFrame.size,
|
|
1272
|
+
};
|
|
1273
|
+
try {
|
|
1274
|
+
if (typeof UIView.animateWithDurationDelayOptionsAnimationsCompletion === 'function') {
|
|
1275
|
+
// UIViewAnimationOptionCurveEaseIn = 1 << 16 — accelerate
|
|
1276
|
+
// out so the dismissal doesn't drag on screen.
|
|
1277
|
+
UIView.animateWithDurationDelayOptionsAnimationsCompletion(0.22, 0, 1 << 16, () => {
|
|
1278
|
+
refs.panel.frame = targetFrame;
|
|
1279
|
+
refs.panel.alpha = 0;
|
|
1280
|
+
}, () => onComplete());
|
|
1281
|
+
}
|
|
1282
|
+
else if (typeof UIView.animateWithDurationAnimationsCompletion === 'function') {
|
|
1283
|
+
UIView.animateWithDurationAnimationsCompletion(0.22, () => {
|
|
1284
|
+
refs.panel.frame = targetFrame;
|
|
1285
|
+
refs.panel.alpha = 0;
|
|
1286
|
+
}, () => onComplete());
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
refs.panel.alpha = 0;
|
|
1290
|
+
onComplete();
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
refs.panel.alpha = 0;
|
|
1295
|
+
onComplete();
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
catch {
|
|
1299
|
+
onComplete();
|
|
949
1300
|
}
|
|
950
1301
|
}
|
|
951
1302
|
function applySnapshotToIosRefs(refs, snapshot) {
|
|
@@ -958,9 +1309,38 @@ function applySnapshotToIosRefs(refs, snapshot) {
|
|
|
958
1309
|
// lazily (ensureIosOverlayRefs) and reused for the lifetime of
|
|
959
1310
|
// the dev session.
|
|
960
1311
|
const visible = snapshot.visible && (snapshot.mode === 'connection' || snapshot.mode === 'update');
|
|
961
|
-
|
|
962
|
-
|
|
1312
|
+
const wasVisible = !!refs.wasVisible;
|
|
1313
|
+
const position = getHmrDevOverlayPosition();
|
|
1314
|
+
const previousPosition = refs.currentPosition;
|
|
1315
|
+
const isToast = position !== 'center';
|
|
1316
|
+
// Touches pass through the overlay window in toast mode so
|
|
1317
|
+
// the user can keep tapping the app while the HMR chip is
|
|
1318
|
+
// shown. In centered mode we keep the blocking
|
|
1319
|
+
// behaviour (the dim backdrop is itself a hint to wait).
|
|
1320
|
+
try {
|
|
1321
|
+
refs.window.userInteractionEnabled = !isToast;
|
|
1322
|
+
}
|
|
1323
|
+
catch { }
|
|
1324
|
+
if (!visible) {
|
|
1325
|
+
// Animate out before hiding the window so the dismissal
|
|
1326
|
+
// has a discoverable motion. Only animate when previously
|
|
1327
|
+
// visible and in toast mode — centered modal hides instantly.
|
|
1328
|
+
if (wasVisible && isToast) {
|
|
1329
|
+
animateIosPanelOut(refs, previousPosition, () => {
|
|
1330
|
+
try {
|
|
1331
|
+
refs.window.hidden = true;
|
|
1332
|
+
}
|
|
1333
|
+
catch { }
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
refs.window.hidden = true;
|
|
1338
|
+
}
|
|
1339
|
+
refs.wasVisible = false;
|
|
1340
|
+
refs.currentPosition = position;
|
|
963
1341
|
return true;
|
|
1342
|
+
}
|
|
1343
|
+
refs.window.hidden = false;
|
|
964
1344
|
refs.titleLabel.text = snapshot.title || '';
|
|
965
1345
|
refs.statusLabel.text = formatStatusText(snapshot);
|
|
966
1346
|
const host = getIosOverlayHost();
|
|
@@ -974,9 +1354,6 @@ function applySnapshotToIosRefs(refs, snapshot) {
|
|
|
974
1354
|
refs.panel.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(1, 0.96, 0.96, 1);
|
|
975
1355
|
refs.titleLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.7, 0.1, 0.06, 1);
|
|
976
1356
|
refs.statusLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.7, 0.1, 0.06, 0.9);
|
|
977
|
-
// Slightly stronger dimming on errors; users need to
|
|
978
|
-
// notice these.
|
|
979
|
-
refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0, 0, 0.35);
|
|
980
1357
|
}
|
|
981
1358
|
else if (isSuccess) {
|
|
982
1359
|
// Slightly more saturated green panel + dark-green
|
|
@@ -988,24 +1365,52 @@ function applySnapshotToIosRefs(refs, snapshot) {
|
|
|
988
1365
|
refs.panel.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0.9, 0.97, 0.91, 1);
|
|
989
1366
|
refs.titleLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.05, 0.43, 0.18, 1);
|
|
990
1367
|
refs.statusLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.05, 0.43, 0.18, 1);
|
|
991
|
-
// Bumped from 0.12 to 0.28. The 0.12 wash was so
|
|
992
|
-
// faint on bright app backgrounds that the overlay
|
|
993
|
-
// was effectively invisible during a fast cycle.
|
|
994
|
-
// 0.28 still keeps the app readable underneath but
|
|
995
|
-
// makes the HMR event visually unmistakable.
|
|
996
|
-
refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0.15, 0.05, 0.28);
|
|
997
1368
|
}
|
|
998
1369
|
else {
|
|
999
1370
|
// Default (info / warn) — existing connection look.
|
|
1000
1371
|
refs.panel.backgroundColor = UIColor.whiteColor;
|
|
1001
1372
|
refs.titleLabel.textColor = UIColor.blackColor;
|
|
1002
1373
|
refs.statusLabel.textColor = UIColor.darkGrayColor;
|
|
1374
|
+
}
|
|
1375
|
+
// Backdrop dims only in centered mode; toast mode keeps
|
|
1376
|
+
// the rest of the app fully visible/usable. Errors get
|
|
1377
|
+
// a slightly stronger dim in centered mode because the
|
|
1378
|
+
// user MUST notice them.
|
|
1379
|
+
if (isToast) {
|
|
1380
|
+
refs.backdrop.backgroundColor = UIColor.clearColor;
|
|
1381
|
+
}
|
|
1382
|
+
else if (isError) {
|
|
1003
1383
|
refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0, 0, 0.35);
|
|
1004
1384
|
}
|
|
1385
|
+
else if (isSuccess) {
|
|
1386
|
+
refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0.15, 0.05, 0.28);
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0, 0, 0.35);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
catch { }
|
|
1393
|
+
}
|
|
1394
|
+
const layout = layoutIosOverlayRefs(refs, position);
|
|
1395
|
+
// Slide-in animation only fires on the actual hidden→visible
|
|
1396
|
+
// transition (or on a position swap — e.g. dev toggling top
|
|
1397
|
+
// to bottom mid-cycle). Subsequent updates within the same
|
|
1398
|
+
// visible cycle just refresh text/colours without re-animating.
|
|
1399
|
+
const positionChanged = previousPosition !== position;
|
|
1400
|
+
const justAppeared = !wasVisible || positionChanged;
|
|
1401
|
+
if (justAppeared && isToast && layout) {
|
|
1402
|
+
animateIosPanelIn(refs, position, layout);
|
|
1403
|
+
}
|
|
1404
|
+
else if (justAppeared && !isToast) {
|
|
1405
|
+
// Centered modal: ensure alpha is reset to 1 in case a
|
|
1406
|
+
// previous toast-mode dismissal left it at 0.
|
|
1407
|
+
try {
|
|
1408
|
+
refs.panel.alpha = 1;
|
|
1005
1409
|
}
|
|
1006
1410
|
catch { }
|
|
1007
1411
|
}
|
|
1008
|
-
|
|
1412
|
+
refs.wasVisible = true;
|
|
1413
|
+
refs.currentPosition = position;
|
|
1009
1414
|
return true;
|
|
1010
1415
|
}
|
|
1011
1416
|
catch (err) {
|