@nativescript/vite 8.0.0-alpha.12 → 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.
@@ -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, centered placement, safe-area clamping, sane defaults)
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 centered panel inside the page;
687
- // only the colours and text change with the snapshot's tone.
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
- refs.overlay.visibility = visible ? 'visible' : 'collapse';
690
- // Backdrop tints by tone:
691
- // error → red wash (matches existing UX)
692
- // success richer green wash; the previous 18% alpha was so
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
- const panel = refs.titleLabel.parent;
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, centered placement, safe-area clamping, sane defaults)
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
- // Center vertically, but never cross the top safe-area inset (notch/Dynamic Island).
768
- const centered = (viewHeight - panelHeight) / 2;
769
- const panelY = Math.max(safeInsets.top + minTopInset, centered);
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
- return { window, controller, backdrop, panel, titleLabel, statusLabel };
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
- refs.window.hidden = !visible;
962
- if (!visible)
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
- layoutIosOverlayRefs(refs);
1412
+ refs.wasVisible = true;
1413
+ refs.currentPosition = position;
1009
1414
  return true;
1010
1415
  }
1011
1416
  catch (err) {