@nativescript/vite 8.0.0-alpha.12 → 8.0.0-alpha.14
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.js +12 -8
- package/configuration/angular.js.map +1 -1
- package/helpers/main-entry.d.ts +2 -1
- package/helpers/main-entry.js +27 -0
- package/helpers/main-entry.js.map +1 -1
- package/hmr/client/css-handler.d.ts +1 -0
- package/hmr/client/css-handler.js +33 -5
- package/hmr/client/css-handler.js.map +1 -1
- package/hmr/client/css-update-overlay.d.ts +18 -0
- package/hmr/client/css-update-overlay.js +27 -0
- package/hmr/client/css-update-overlay.js.map +1 -0
- package/hmr/client/index.js +20 -0
- package/hmr/client/index.js.map +1 -1
- package/hmr/entry-runtime.d.ts +1 -0
- package/hmr/entry-runtime.js +62 -5
- package/hmr/entry-runtime.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/vite-plugin.js +24 -4
- package/hmr/server/vite-plugin.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-css-hot-update.d.ts +33 -0
- package/hmr/server/websocket-css-hot-update.js +65 -0
- package/hmr/server/websocket-css-hot-update.js.map +1 -0
- package/hmr/server/websocket-served-module-helpers.js +19 -6
- package/hmr/server/websocket-served-module-helpers.js.map +1 -1
- package/hmr/server/websocket.d.ts +1 -0
- package/hmr/server/websocket.js +87 -48
- package/hmr/server/websocket.js.map +1 -1
- package/hmr/shared/runtime/boot-placeholder-ui.d.ts +69 -0
- package/hmr/shared/runtime/boot-placeholder-ui.js +101 -0
- package/hmr/shared/runtime/boot-placeholder-ui.js.map +1 -0
- package/hmr/shared/runtime/boot-progress.d.ts +40 -0
- package/hmr/shared/runtime/boot-progress.js +128 -0
- package/hmr/shared/runtime/boot-progress.js.map +1 -0
- package/hmr/shared/runtime/boot-timeline.d.ts +1 -0
- package/hmr/shared/runtime/boot-timeline.js +1 -0
- package/hmr/shared/runtime/boot-timeline.js.map +1 -1
- package/hmr/shared/runtime/dev-overlay.d.ts +30 -2
- package/hmr/shared/runtime/dev-overlay.js +539 -43
- package/hmr/shared/runtime/dev-overlay.js.map +1 -1
- package/hmr/shared/runtime/root-placeholder.js +317 -47
- package/hmr/shared/runtime/root-placeholder.js.map +1 -1
- package/hmr/shared/runtime/session-bootstrap.js +132 -18
- package/hmr/shared/runtime/session-bootstrap.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { BOOT_PLACEHOLDER_MOTION, computeBootProgressFillScale, formatBootDetailLine, formatBootPrimaryLine } from './boot-placeholder-ui.js';
|
|
2
|
+
const DEFAULT_OVERLAY_POSITION = 'top';
|
|
1
3
|
const BOOT_TITLE = 'NativeScript Vite preparing dev session...';
|
|
2
4
|
const DEFAULT_SNAPSHOT = {
|
|
3
5
|
visible: false,
|
|
@@ -14,6 +16,37 @@ const DEFAULT_SNAPSHOT = {
|
|
|
14
16
|
function getOverlayGlobal() {
|
|
15
17
|
return globalThis;
|
|
16
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the configured live-overlay position.
|
|
21
|
+
*
|
|
22
|
+
* Reads `globalThis.__NS_HMR_OVERLAY_POSITION__` so a project can
|
|
23
|
+
* override the default at boot time (e.g. inside `app.ts` before the
|
|
24
|
+
* Vite session bootstraps). Falls back to 'top' which gives the
|
|
25
|
+
* toast-style chip with a slide-in animation and safe-area padding.
|
|
26
|
+
*/
|
|
27
|
+
export function getHmrDevOverlayPosition() {
|
|
28
|
+
const g = getOverlayGlobal();
|
|
29
|
+
const stored = g.__NS_HMR_OVERLAY_POSITION__;
|
|
30
|
+
if (stored === 'top' || stored === 'bottom' || stored === 'center') {
|
|
31
|
+
return stored;
|
|
32
|
+
}
|
|
33
|
+
return DEFAULT_OVERLAY_POSITION;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Imperative setter for the live-overlay position. Re-applies the
|
|
37
|
+
* current snapshot so the change is visible without waiting for the
|
|
38
|
+
* next HMR cycle. Useful during dev to A/B between top/bottom/center
|
|
39
|
+
* without restarting the app.
|
|
40
|
+
*/
|
|
41
|
+
export function setHmrDevOverlayPosition(position) {
|
|
42
|
+
if (position !== 'top' && position !== 'bottom' && position !== 'center') {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const g = getOverlayGlobal();
|
|
46
|
+
g.__NS_HMR_OVERLAY_POSITION__ = position;
|
|
47
|
+
const state = getRuntimeState();
|
|
48
|
+
applyRuntimeSnapshot(state.snapshot);
|
|
49
|
+
}
|
|
17
50
|
function getRuntimeState() {
|
|
18
51
|
const g = getOverlayGlobal();
|
|
19
52
|
if (!g.__NS_HMR_DEV_OVERLAY_STATE__) {
|
|
@@ -146,7 +179,11 @@ export function createBootOverlaySnapshot(stage, info) {
|
|
|
146
179
|
badge: 'BOOT',
|
|
147
180
|
title: BOOT_TITLE,
|
|
148
181
|
phase: 'Importing the app entry',
|
|
149
|
-
|
|
182
|
+
// 30 (not 82) so the bar visibly climbs the ~62 points the
|
|
183
|
+
// heartbeat + snippet drive during the long HTTP-module-load
|
|
184
|
+
// phase. The monotonic ratchet in `setBootStage` prevents
|
|
185
|
+
// earlier-but-higher stages from being clobbered.
|
|
186
|
+
progress: 30,
|
|
150
187
|
busy: true,
|
|
151
188
|
blocking: true,
|
|
152
189
|
tone: 'info',
|
|
@@ -533,10 +570,28 @@ function findBootStatusLabel() {
|
|
|
533
570
|
catch { }
|
|
534
571
|
return null;
|
|
535
572
|
}
|
|
573
|
+
function findBootDetailLabel() {
|
|
574
|
+
const g = getOverlayGlobal();
|
|
575
|
+
return g.__NS_DEV_BOOT_DETAIL_LABEL__ || null;
|
|
576
|
+
}
|
|
577
|
+
function findBootProgressFill() {
|
|
578
|
+
const g = getOverlayGlobal();
|
|
579
|
+
return g.__NS_DEV_BOOT_PROGRESS_FILL__ || null;
|
|
580
|
+
}
|
|
536
581
|
function updateBootStatusLabel(snapshot) {
|
|
537
|
-
const newText = formatStatusText(snapshot) || 'Preparing the HTTP HMR bootstrap (4%)';
|
|
538
582
|
const statusLabel = findBootStatusLabel();
|
|
583
|
+
const detailLabel = findBootDetailLabel();
|
|
584
|
+
const progressFill = findBootProgressFill();
|
|
539
585
|
const activityIndicator = findBootActivityIndicator();
|
|
586
|
+
// New (card) layout: phase line + detail line live in separate
|
|
587
|
+
// labels so the typography can differ. Legacy (single-label)
|
|
588
|
+
// layout: keep the original combined "phase (X%)\ndetail" text so
|
|
589
|
+
// nothing visually regresses for runtimes still attached to the
|
|
590
|
+
// older placeholder shape.
|
|
591
|
+
const hasSplitLabels = !!detailLabel;
|
|
592
|
+
const phaseLine = formatBootPrimaryLine(snapshot);
|
|
593
|
+
const detailLine = formatBootDetailLine(snapshot);
|
|
594
|
+
const combinedText = formatStatusText(snapshot) || 'Preparing the HTTP HMR bootstrap (4%)';
|
|
540
595
|
if (!statusLabel) {
|
|
541
596
|
if (activityIndicator) {
|
|
542
597
|
try {
|
|
@@ -545,11 +600,16 @@ function updateBootStatusLabel(snapshot) {
|
|
|
545
600
|
}
|
|
546
601
|
catch { }
|
|
547
602
|
}
|
|
603
|
+
applyBootProgressFill(progressFill, snapshot);
|
|
548
604
|
return;
|
|
549
605
|
}
|
|
550
606
|
try {
|
|
551
|
-
statusLabel.text =
|
|
552
|
-
|
|
607
|
+
statusLabel.text = hasSplitLabels ? phaseLine || 'Preparing the HTTP HMR bootstrap' : combinedText;
|
|
608
|
+
// Card layout uses the calibrated phase-text colour from the
|
|
609
|
+
// palette; legacy single-label layout keeps the original muted
|
|
610
|
+
// brown so we don't visually regress mid-session.
|
|
611
|
+
const phaseColorHex = snapshot.tone === 'error' ? '#B91C1C' : hasSplitLabels ? '#475569' : '#563e3fb1';
|
|
612
|
+
statusLabel.color = asColor(phaseColorHex);
|
|
553
613
|
if (typeof statusLabel.requestLayout === 'function') {
|
|
554
614
|
statusLabel.requestLayout();
|
|
555
615
|
}
|
|
@@ -559,6 +619,15 @@ function updateBootStatusLabel(snapshot) {
|
|
|
559
619
|
}
|
|
560
620
|
}
|
|
561
621
|
catch { }
|
|
622
|
+
if (detailLabel) {
|
|
623
|
+
try {
|
|
624
|
+
detailLabel.text = detailLine;
|
|
625
|
+
detailLabel.color = asColor(snapshot.tone === 'error' ? '#DC2626' : '#94A3B8');
|
|
626
|
+
detailLabel.visibility = detailLine ? 'visible' : 'collapse';
|
|
627
|
+
}
|
|
628
|
+
catch { }
|
|
629
|
+
}
|
|
630
|
+
applyBootProgressFill(progressFill, snapshot);
|
|
562
631
|
if (activityIndicator) {
|
|
563
632
|
try {
|
|
564
633
|
activityIndicator.busy = !!snapshot.busy;
|
|
@@ -567,6 +636,50 @@ function updateBootStatusLabel(snapshot) {
|
|
|
567
636
|
catch { }
|
|
568
637
|
}
|
|
569
638
|
}
|
|
639
|
+
// Drive the progress fill scaleX from the snapshot. Uses NS's view
|
|
640
|
+
// animate API for a smooth 220 ms easeOut between heartbeat ticks; a
|
|
641
|
+
// monotonic ratchet on `globalThis.__NS_DEV_BOOT_PROGRESS_LAST_SCALE__`
|
|
642
|
+
// guards against the fill snapping backwards if a less-progressed
|
|
643
|
+
// snapshot ever lands between ticks (mirrors the JS-side
|
|
644
|
+
// `applyMonotonicBootProgress` contract).
|
|
645
|
+
function applyBootProgressFill(progressFill, snapshot) {
|
|
646
|
+
if (!progressFill)
|
|
647
|
+
return;
|
|
648
|
+
const g = getOverlayGlobal();
|
|
649
|
+
const isError = snapshot.tone === 'error';
|
|
650
|
+
progressFill.backgroundColor = asColor(isError ? '#B41810' : '#3B6FE5');
|
|
651
|
+
const targetScale = computeBootProgressFillScale(snapshot.progress ?? null);
|
|
652
|
+
const previousRaw = Number(g.__NS_DEV_BOOT_PROGRESS_LAST_SCALE__);
|
|
653
|
+
const previous = Number.isFinite(previousRaw) ? previousRaw : 0;
|
|
654
|
+
const next = Math.max(previous, targetScale);
|
|
655
|
+
g.__NS_DEV_BOOT_PROGRESS_LAST_SCALE__ = next;
|
|
656
|
+
try {
|
|
657
|
+
// NS view.animate scales around `originX`/`originY`; the
|
|
658
|
+
// placeholder builder pins `originX = 0` so the fill grows
|
|
659
|
+
// rightward. animate() may be unavailable in some headless
|
|
660
|
+
// test environments — fall through to a direct property set.
|
|
661
|
+
if (typeof progressFill.animate === 'function') {
|
|
662
|
+
progressFill
|
|
663
|
+
.animate({
|
|
664
|
+
scale: { x: next, y: 1 },
|
|
665
|
+
duration: BOOT_PLACEHOLDER_MOTION.progressDurationMs,
|
|
666
|
+
curve: 'easeOut',
|
|
667
|
+
})
|
|
668
|
+
.catch(() => {
|
|
669
|
+
try {
|
|
670
|
+
progressFill.scaleX = next;
|
|
671
|
+
}
|
|
672
|
+
catch { }
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
progressFill.scaleX = next;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
progressFill.scaleX = next;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
570
683
|
function resolveActivePage() {
|
|
571
684
|
try {
|
|
572
685
|
const Frame = resolveCoreExport('Frame');
|
|
@@ -603,8 +716,18 @@ function buildLiveOverlayView(snapshot) {
|
|
|
603
716
|
overlay.height = '100%';
|
|
604
717
|
overlay.horizontalAlignment = 'stretch';
|
|
605
718
|
overlay.verticalAlignment = 'stretch';
|
|
719
|
+
// Toast mode lets touches reach the underlying app. We flip
|
|
720
|
+
// isUserInteractionEnabled in applySnapshotToLiveRefs based on
|
|
721
|
+
// the resolved position, but keep it false here as a safe default
|
|
722
|
+
// (the panel itself is purely informational).
|
|
723
|
+
try {
|
|
724
|
+
overlay.isUserInteractionEnabled = false;
|
|
725
|
+
}
|
|
726
|
+
catch { }
|
|
606
727
|
const panel = new StackLayout();
|
|
607
728
|
panel.horizontalAlignment = 'center';
|
|
729
|
+
// Vertical alignment is overridden in applySnapshotToLiveRefs
|
|
730
|
+
// based on getHmrDevOverlayPosition(); 'middle' is the default
|
|
608
731
|
panel.verticalAlignment = 'middle';
|
|
609
732
|
panel.width = 320;
|
|
610
733
|
panel.margin = 24;
|
|
@@ -626,6 +749,8 @@ function buildLiveOverlayView(snapshot) {
|
|
|
626
749
|
overlay,
|
|
627
750
|
titleLabel,
|
|
628
751
|
statusLabel,
|
|
752
|
+
wasVisible: false,
|
|
753
|
+
currentPosition: getHmrDevOverlayPosition(),
|
|
629
754
|
};
|
|
630
755
|
applySnapshotToLiveRefs(refs, snapshot);
|
|
631
756
|
return refs;
|
|
@@ -683,32 +808,42 @@ function applySnapshotToLiveRefs(refs, snapshot) {
|
|
|
683
808
|
return;
|
|
684
809
|
}
|
|
685
810
|
// 'update' mode shares the live (in-tree) overlay chrome with
|
|
686
|
-
// 'connection'. Both render a
|
|
687
|
-
//
|
|
811
|
+
// 'connection'. Both render a small panel inside the page; only
|
|
812
|
+
// the colours, text, and (now) panel position change with the
|
|
813
|
+
// snapshot's tone and the configured overlay position.
|
|
688
814
|
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);
|
|
815
|
+
const wasVisible = !!refs.wasVisible;
|
|
816
|
+
const position = getHmrDevOverlayPosition();
|
|
817
|
+
const previousPosition = refs.currentPosition || position;
|
|
818
|
+
const isToast = position !== 'center';
|
|
700
819
|
refs.titleLabel.text = snapshot.title;
|
|
820
|
+
refs.statusLabel.text = formatStatusText(snapshot);
|
|
701
821
|
const textColor = snapshot.tone === 'error' ? '#b41810e6' : snapshot.tone === 'success' ? '#0e6e2fff' : '#563e3fb1';
|
|
702
822
|
refs.titleLabel.color = asColor(textColor);
|
|
703
|
-
refs.statusLabel.text = formatStatusText(snapshot);
|
|
704
823
|
refs.statusLabel.color = asColor(textColor);
|
|
824
|
+
// Backdrop tints (centered modal only). Toast modes use a fully
|
|
825
|
+
// transparent backdrop so the rest of the app stays visible AND
|
|
826
|
+
// reachable; the panel itself carries enough colour to stand out.
|
|
827
|
+
if (isToast) {
|
|
828
|
+
refs.overlay.backgroundColor = asColor('transparent');
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
// Original wash-by-tone for centered:
|
|
832
|
+
// error → red wash (matches existing UX)
|
|
833
|
+
// success → richer green wash so the apply event is visible
|
|
834
|
+
// on bright app backgrounds
|
|
835
|
+
// default → warm orange (existing connection-overlay look)
|
|
836
|
+
const overlayBg = snapshot.tone === 'error' ? '#b4181068' : snapshot.tone === 'success' ? '#1f883d80' : '#a1771683';
|
|
837
|
+
refs.overlay.backgroundColor = asColor(overlayBg);
|
|
838
|
+
}
|
|
839
|
+
// Panel chrome — toast and centered share the same chip look,
|
|
840
|
+
// just position differs. We keep the slightly richer green tint
|
|
841
|
+
// for the HMR success state so it pops without needing the
|
|
842
|
+
// backdrop wash.
|
|
843
|
+
let panel = null;
|
|
705
844
|
try {
|
|
706
|
-
|
|
845
|
+
panel = refs.titleLabel.parent;
|
|
707
846
|
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
847
|
const panelBg = snapshot.tone === 'success' ? '#E6F8E9FF' : '#FFFFFFFF';
|
|
713
848
|
panel.backgroundColor = asColor(panelBg);
|
|
714
849
|
panel.opacity = 1;
|
|
@@ -717,9 +852,142 @@ function applySnapshotToLiveRefs(refs, snapshot) {
|
|
|
717
852
|
panel.borderRadius = 12;
|
|
718
853
|
}
|
|
719
854
|
catch { }
|
|
855
|
+
// Position-aware alignment. The wrapper GridLayout fills
|
|
856
|
+
// the page content area, which on iOS is already inside
|
|
857
|
+
// the safe area; we add a small extra margin so the chip
|
|
858
|
+
// doesn't kiss the notch / home indicator.
|
|
859
|
+
try {
|
|
860
|
+
if (position === 'top') {
|
|
861
|
+
panel.verticalAlignment = 'top';
|
|
862
|
+
panel.margin = '12 16 0 16';
|
|
863
|
+
}
|
|
864
|
+
else if (position === 'bottom') {
|
|
865
|
+
panel.verticalAlignment = 'bottom';
|
|
866
|
+
panel.margin = '0 16 12 16';
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
panel.verticalAlignment = 'middle';
|
|
870
|
+
panel.margin = 24;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
catch { }
|
|
720
874
|
}
|
|
721
875
|
}
|
|
722
876
|
catch { }
|
|
877
|
+
// Touch passthrough for toast; centered mode keeps the
|
|
878
|
+
// blocking modal so the dim backdrop is meaningful.
|
|
879
|
+
try {
|
|
880
|
+
refs.overlay.isUserInteractionEnabled = !isToast;
|
|
881
|
+
}
|
|
882
|
+
catch { }
|
|
883
|
+
const positionChanged = previousPosition !== position;
|
|
884
|
+
const justAppeared = visible && (!wasVisible || positionChanged);
|
|
885
|
+
const justDismissed = !visible && wasVisible;
|
|
886
|
+
if (justAppeared) {
|
|
887
|
+
refs.overlay.visibility = 'visible';
|
|
888
|
+
if (isToast && panel && typeof panel.animate === 'function') {
|
|
889
|
+
animateLivePanelIn(panel, position);
|
|
890
|
+
}
|
|
891
|
+
else if (panel) {
|
|
892
|
+
try {
|
|
893
|
+
panel.translateY = 0;
|
|
894
|
+
panel.opacity = 1;
|
|
895
|
+
}
|
|
896
|
+
catch { }
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
else if (justDismissed) {
|
|
900
|
+
if (isToast && panel && typeof panel.animate === 'function') {
|
|
901
|
+
animateLivePanelOut(panel, previousPosition, () => {
|
|
902
|
+
try {
|
|
903
|
+
refs.overlay.visibility = 'collapse';
|
|
904
|
+
}
|
|
905
|
+
catch { }
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
refs.overlay.visibility = 'collapse';
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
refs.overlay.visibility = visible ? 'visible' : 'collapse';
|
|
914
|
+
}
|
|
915
|
+
if (typeof refs.wasVisible !== 'undefined')
|
|
916
|
+
refs.wasVisible = visible;
|
|
917
|
+
if (typeof refs.currentPosition !== 'undefined')
|
|
918
|
+
refs.currentPosition = position;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Slide-in animation for the in-tree toast panel.
|
|
922
|
+
*
|
|
923
|
+
* NativeScript's `View.animate({ translate, opacity, duration, curve })`
|
|
924
|
+
* is widely available across Core versions, so we don't depend on any
|
|
925
|
+
* specific curve enum being importable here. We use a moderate-to-snappy
|
|
926
|
+
* 320ms ease-out which feels close to a UIView spring without needing
|
|
927
|
+
* platform-specific APIs.
|
|
928
|
+
*/
|
|
929
|
+
function animateLivePanelIn(panel, position) {
|
|
930
|
+
if (!panel || typeof panel.animate !== 'function')
|
|
931
|
+
return;
|
|
932
|
+
try {
|
|
933
|
+
const startY = position === 'bottom' ? 80 : -80;
|
|
934
|
+
panel.translateY = startY;
|
|
935
|
+
panel.opacity = 0;
|
|
936
|
+
const result = panel.animate({
|
|
937
|
+
translate: { x: 0, y: 0 },
|
|
938
|
+
opacity: 1,
|
|
939
|
+
duration: 320,
|
|
940
|
+
curve: 'easeOut',
|
|
941
|
+
});
|
|
942
|
+
if (result && typeof result.catch === 'function') {
|
|
943
|
+
result.catch(() => {
|
|
944
|
+
try {
|
|
945
|
+
panel.translateY = 0;
|
|
946
|
+
panel.opacity = 1;
|
|
947
|
+
}
|
|
948
|
+
catch { }
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
try {
|
|
954
|
+
panel.translateY = 0;
|
|
955
|
+
panel.opacity = 1;
|
|
956
|
+
}
|
|
957
|
+
catch { }
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function animateLivePanelOut(panel, position, onComplete) {
|
|
961
|
+
if (!panel || typeof panel.animate !== 'function') {
|
|
962
|
+
onComplete();
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
const targetY = position === 'bottom' ? 80 : -80;
|
|
967
|
+
const result = panel.animate({
|
|
968
|
+
translate: { x: 0, y: targetY },
|
|
969
|
+
opacity: 0,
|
|
970
|
+
duration: 220,
|
|
971
|
+
curve: 'easeIn',
|
|
972
|
+
});
|
|
973
|
+
const finish = () => {
|
|
974
|
+
try {
|
|
975
|
+
panel.translateY = 0;
|
|
976
|
+
panel.opacity = 1;
|
|
977
|
+
}
|
|
978
|
+
catch { }
|
|
979
|
+
onComplete();
|
|
980
|
+
};
|
|
981
|
+
if (result && typeof result.then === 'function') {
|
|
982
|
+
result.then(finish, finish);
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
finish();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
onComplete();
|
|
990
|
+
}
|
|
723
991
|
}
|
|
724
992
|
// pure helpers for iOS window promotion. Factored out so the layout
|
|
725
993
|
// math and window-level selection stay unit-testable without booting a
|
|
@@ -739,8 +1007,17 @@ export function computeIosOverlayWindowLevel(baseAlert) {
|
|
|
739
1007
|
/**
|
|
740
1008
|
* Layout math for the live overlay when it runs inside its own UIWindow.
|
|
741
1009
|
* Pure, deterministic and independent of UIKit so we can verify the rules
|
|
742
|
-
* (max panel width,
|
|
743
|
-
* from tests.
|
|
1010
|
+
* (max panel width, position-aware placement, safe-area clamping, sane
|
|
1011
|
+
* defaults) from tests.
|
|
1012
|
+
*
|
|
1013
|
+
* `position` controls where the panel sits vertically:
|
|
1014
|
+
* - 'top': hugs `safeInsets.top + toastVerticalInset` so the chip
|
|
1015
|
+
* sits just below the notch / Dynamic Island.
|
|
1016
|
+
* - 'bottom': hugs `viewHeight - safeInsets.bottom - panelHeight -
|
|
1017
|
+
* toastVerticalInset` so the chip sits just above the
|
|
1018
|
+
* home indicator / nav bar.
|
|
1019
|
+
* - 'center': original modal placement (vertically centered, clamped
|
|
1020
|
+
* so it never crosses the top safe-area inset).
|
|
744
1021
|
*/
|
|
745
1022
|
export function computeIosOverlayLayout(input) {
|
|
746
1023
|
const viewWidth = Math.max(0, Number(input.viewWidth) || 0);
|
|
@@ -758,15 +1035,41 @@ export function computeIosOverlayLayout(input) {
|
|
|
758
1035
|
const panelPadding = Math.max(0, Number(input.panelPadding ?? 16));
|
|
759
1036
|
const interLabelSpacing = Math.max(0, Number(input.interLabelSpacing ?? 10));
|
|
760
1037
|
const minTopInset = Math.max(0, Number(input.minTopInset ?? 20));
|
|
1038
|
+
// Default to 'center' on the pure function so the existing
|
|
1039
|
+
// snapshot/layout tests remain stable; the runtime call site
|
|
1040
|
+
// (layoutIosOverlayRefs) reads the configured position from
|
|
1041
|
+
// `getHmrDevOverlayPosition()` and forwards it explicitly.
|
|
1042
|
+
const position = input.position ?? 'center';
|
|
1043
|
+
// Distance between the panel and the safe-area edge in toast
|
|
1044
|
+
// modes. 8pt mirrors the typical iOS notification chip inset and
|
|
1045
|
+
// keeps the chip from hugging the notch / home indicator.
|
|
1046
|
+
const toastVerticalInset = Math.max(0, Number(input.toastVerticalInset ?? 8));
|
|
761
1047
|
const available = Math.max(0, viewWidth - 2 * horizontalMargin - safeInsets.left - safeInsets.right);
|
|
762
1048
|
const panelWidth = Math.min(maxPanelWidth, available);
|
|
763
1049
|
const innerWidth = Math.max(0, panelWidth - 2 * panelPadding);
|
|
764
1050
|
const spacing = titleHeight > 0 && statusHeight > 0 ? interLabelSpacing : 0;
|
|
765
1051
|
const panelHeight = panelPadding * 2 + titleHeight + spacing + statusHeight;
|
|
766
1052
|
const panelX = Math.max(0, (viewWidth - panelWidth) / 2);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1053
|
+
let panelY;
|
|
1054
|
+
if (position === 'top') {
|
|
1055
|
+
// Pin to the top safe-area inset (just below notch / Dynamic
|
|
1056
|
+
// Island). Clamp non-negative for fully-NaN input.
|
|
1057
|
+
panelY = Math.max(0, safeInsets.top + toastVerticalInset);
|
|
1058
|
+
}
|
|
1059
|
+
else if (position === 'bottom') {
|
|
1060
|
+
// Pin to the bottom safe-area inset (just above home indicator
|
|
1061
|
+
// / nav bar). If the panel can't fit between the safe-area
|
|
1062
|
+
// insets we fall back to the top safe-area edge so the chip is
|
|
1063
|
+
// always visible (rather than getting clipped off-screen).
|
|
1064
|
+
const desired = viewHeight - safeInsets.bottom - panelHeight - toastVerticalInset;
|
|
1065
|
+
panelY = Math.max(safeInsets.top + minTopInset, desired);
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
// Center vertically, but never cross the top safe-area inset
|
|
1069
|
+
// (notch/Dynamic Island). Original modal placement.
|
|
1070
|
+
const centered = (viewHeight - panelHeight) / 2;
|
|
1071
|
+
panelY = Math.max(safeInsets.top + minTopInset, centered);
|
|
1072
|
+
}
|
|
770
1073
|
return {
|
|
771
1074
|
backdrop: { x: 0, y: 0, width: viewWidth, height: viewHeight },
|
|
772
1075
|
panel: { x: panelX, y: panelY, width: panelWidth, height: panelHeight },
|
|
@@ -880,7 +1183,32 @@ function buildIosOverlayRefs(state) {
|
|
|
880
1183
|
statusLabel.font = UIFont.systemFontOfSize(13);
|
|
881
1184
|
statusLabel.textColor = UIColor.darkGrayColor;
|
|
882
1185
|
panel.addSubview(statusLabel);
|
|
883
|
-
|
|
1186
|
+
// Subtle drop-shadow so the toast chip reads against light app
|
|
1187
|
+
// content (white-on-white is invisible). The error / centered
|
|
1188
|
+
// branches still get the dim backdrop, so the shadow is mostly
|
|
1189
|
+
// a no-op for them — but it's a one-time setup.
|
|
1190
|
+
try {
|
|
1191
|
+
panel.layer.shadowColor = UIColor.blackColor.CGColor;
|
|
1192
|
+
panel.layer.shadowOpacity = 0.18;
|
|
1193
|
+
panel.layer.shadowRadius = 8;
|
|
1194
|
+
panel.layer.shadowOffset = { width: 0, height: 2 };
|
|
1195
|
+
panel.layer.masksToBounds = false;
|
|
1196
|
+
}
|
|
1197
|
+
catch { }
|
|
1198
|
+
// `wasVisible` / `currentPosition` are mutated by
|
|
1199
|
+
// applySnapshotToIosRefs when the snapshot triggers a slide-in
|
|
1200
|
+
// or slide-out. They start in the "hidden" state so the very
|
|
1201
|
+
// first visible snapshot animates in cleanly.
|
|
1202
|
+
return {
|
|
1203
|
+
window,
|
|
1204
|
+
controller,
|
|
1205
|
+
backdrop,
|
|
1206
|
+
panel,
|
|
1207
|
+
titleLabel,
|
|
1208
|
+
statusLabel,
|
|
1209
|
+
wasVisible: false,
|
|
1210
|
+
currentPosition: getHmrDevOverlayPosition(),
|
|
1211
|
+
};
|
|
884
1212
|
}
|
|
885
1213
|
catch (err) {
|
|
886
1214
|
console.warn('[ns-hmr-overlay] iOS overlay construction failed:', err?.message || err);
|
|
@@ -903,7 +1231,7 @@ function ensureIosOverlayRefs(state) {
|
|
|
903
1231
|
}
|
|
904
1232
|
return state.iosRefs;
|
|
905
1233
|
}
|
|
906
|
-
function layoutIosOverlayRefs(refs) {
|
|
1234
|
+
function layoutIosOverlayRefs(refs, position) {
|
|
907
1235
|
try {
|
|
908
1236
|
const bounds = refs.controller.view.bounds;
|
|
909
1237
|
const viewWidth = Number(bounds?.size?.width) || 0;
|
|
@@ -934,6 +1262,7 @@ function layoutIosOverlayRefs(refs) {
|
|
|
934
1262
|
maxPanelWidth,
|
|
935
1263
|
horizontalMargin,
|
|
936
1264
|
panelPadding,
|
|
1265
|
+
position,
|
|
937
1266
|
});
|
|
938
1267
|
const toCgRect = (rect) => ({
|
|
939
1268
|
origin: { x: rect.x, y: rect.y },
|
|
@@ -943,9 +1272,112 @@ function layoutIosOverlayRefs(refs) {
|
|
|
943
1272
|
refs.panel.frame = toCgRect(layout.panel);
|
|
944
1273
|
refs.titleLabel.frame = toCgRect(layout.title);
|
|
945
1274
|
refs.statusLabel.frame = toCgRect(layout.status);
|
|
1275
|
+
return layout;
|
|
946
1276
|
}
|
|
947
1277
|
catch (err) {
|
|
948
1278
|
console.warn('[ns-hmr-overlay] iOS overlay layout failed:', err?.message || err);
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Slide-in animation for the iOS toast panel. Off-screen start frame
|
|
1284
|
+
* lives just above (top) or below (bottom) the visible area; the panel
|
|
1285
|
+
* snaps to its target frame with a spring so the motion feels physical
|
|
1286
|
+
* without the heavy "settle" overshoot of a hard spring (damping 0.85
|
|
1287
|
+
* lands quickly with a small overshoot).
|
|
1288
|
+
*/
|
|
1289
|
+
function animateIosPanelIn(refs, position, layout) {
|
|
1290
|
+
const g = getOverlayGlobal();
|
|
1291
|
+
const UIView = g?.UIView;
|
|
1292
|
+
if (!UIView)
|
|
1293
|
+
return;
|
|
1294
|
+
try {
|
|
1295
|
+
const targetFrame = {
|
|
1296
|
+
origin: { x: layout.panel.x, y: layout.panel.y },
|
|
1297
|
+
size: { width: layout.panel.width, height: layout.panel.height },
|
|
1298
|
+
};
|
|
1299
|
+
// Off-screen start: distance includes a small fudge so the
|
|
1300
|
+
// shadow blur tail isn't visible at t=0.
|
|
1301
|
+
const startY = position === 'bottom' ? layout.backdrop.height + 24 : -(layout.panel.height + 24);
|
|
1302
|
+
refs.panel.frame = {
|
|
1303
|
+
origin: { x: layout.panel.x, y: startY },
|
|
1304
|
+
size: { width: layout.panel.width, height: layout.panel.height },
|
|
1305
|
+
};
|
|
1306
|
+
refs.panel.alpha = 0;
|
|
1307
|
+
try {
|
|
1308
|
+
if (typeof UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion === 'function') {
|
|
1309
|
+
UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(0.42, 0, 0.85, 0.7, 0, () => {
|
|
1310
|
+
refs.panel.frame = targetFrame;
|
|
1311
|
+
refs.panel.alpha = 1;
|
|
1312
|
+
}, null);
|
|
1313
|
+
}
|
|
1314
|
+
else if (typeof UIView.animateWithDurationAnimations === 'function') {
|
|
1315
|
+
UIView.animateWithDurationAnimations(0.32, () => {
|
|
1316
|
+
refs.panel.frame = targetFrame;
|
|
1317
|
+
refs.panel.alpha = 1;
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
refs.panel.frame = targetFrame;
|
|
1322
|
+
refs.panel.alpha = 1;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
catch {
|
|
1326
|
+
refs.panel.frame = targetFrame;
|
|
1327
|
+
refs.panel.alpha = 1;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
catch { }
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Slide-out animation for the iOS toast panel. Mirrors animateIosPanelIn:
|
|
1334
|
+
* the panel travels to the nearest off-screen edge while fading out so
|
|
1335
|
+
* the dismissal still feels intentional even on fast HMR cycles.
|
|
1336
|
+
*/
|
|
1337
|
+
function animateIosPanelOut(refs, position, onComplete) {
|
|
1338
|
+
const g = getOverlayGlobal();
|
|
1339
|
+
const UIView = g?.UIView;
|
|
1340
|
+
const currentFrame = refs.panel?.frame;
|
|
1341
|
+
if (!UIView || !currentFrame) {
|
|
1342
|
+
onComplete();
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
try {
|
|
1346
|
+
const bounds = refs.controller?.view?.bounds;
|
|
1347
|
+
const viewHeight = Number(bounds?.size?.height) || 0;
|
|
1348
|
+
const targetY = position === 'bottom' ? viewHeight + 24 : -(Number(currentFrame.size?.height) + 24);
|
|
1349
|
+
const startFrame = currentFrame;
|
|
1350
|
+
const targetFrame = {
|
|
1351
|
+
origin: { x: Number(startFrame.origin?.x) || 0, y: targetY },
|
|
1352
|
+
size: startFrame.size,
|
|
1353
|
+
};
|
|
1354
|
+
try {
|
|
1355
|
+
if (typeof UIView.animateWithDurationDelayOptionsAnimationsCompletion === 'function') {
|
|
1356
|
+
// UIViewAnimationOptionCurveEaseIn = 1 << 16 — accelerate
|
|
1357
|
+
// out so the dismissal doesn't drag on screen.
|
|
1358
|
+
UIView.animateWithDurationDelayOptionsAnimationsCompletion(0.22, 0, 1 << 16, () => {
|
|
1359
|
+
refs.panel.frame = targetFrame;
|
|
1360
|
+
refs.panel.alpha = 0;
|
|
1361
|
+
}, () => onComplete());
|
|
1362
|
+
}
|
|
1363
|
+
else if (typeof UIView.animateWithDurationAnimationsCompletion === 'function') {
|
|
1364
|
+
UIView.animateWithDurationAnimationsCompletion(0.22, () => {
|
|
1365
|
+
refs.panel.frame = targetFrame;
|
|
1366
|
+
refs.panel.alpha = 0;
|
|
1367
|
+
}, () => onComplete());
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
refs.panel.alpha = 0;
|
|
1371
|
+
onComplete();
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
catch {
|
|
1375
|
+
refs.panel.alpha = 0;
|
|
1376
|
+
onComplete();
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
catch {
|
|
1380
|
+
onComplete();
|
|
949
1381
|
}
|
|
950
1382
|
}
|
|
951
1383
|
function applySnapshotToIosRefs(refs, snapshot) {
|
|
@@ -958,9 +1390,38 @@ function applySnapshotToIosRefs(refs, snapshot) {
|
|
|
958
1390
|
// lazily (ensureIosOverlayRefs) and reused for the lifetime of
|
|
959
1391
|
// the dev session.
|
|
960
1392
|
const visible = snapshot.visible && (snapshot.mode === 'connection' || snapshot.mode === 'update');
|
|
961
|
-
|
|
962
|
-
|
|
1393
|
+
const wasVisible = !!refs.wasVisible;
|
|
1394
|
+
const position = getHmrDevOverlayPosition();
|
|
1395
|
+
const previousPosition = refs.currentPosition;
|
|
1396
|
+
const isToast = position !== 'center';
|
|
1397
|
+
// Touches pass through the overlay window in toast mode so
|
|
1398
|
+
// the user can keep tapping the app while the HMR chip is
|
|
1399
|
+
// shown. In centered mode we keep the blocking
|
|
1400
|
+
// behaviour (the dim backdrop is itself a hint to wait).
|
|
1401
|
+
try {
|
|
1402
|
+
refs.window.userInteractionEnabled = !isToast;
|
|
1403
|
+
}
|
|
1404
|
+
catch { }
|
|
1405
|
+
if (!visible) {
|
|
1406
|
+
// Animate out before hiding the window so the dismissal
|
|
1407
|
+
// has a discoverable motion. Only animate when previously
|
|
1408
|
+
// visible and in toast mode — centered modal hides instantly.
|
|
1409
|
+
if (wasVisible && isToast) {
|
|
1410
|
+
animateIosPanelOut(refs, previousPosition, () => {
|
|
1411
|
+
try {
|
|
1412
|
+
refs.window.hidden = true;
|
|
1413
|
+
}
|
|
1414
|
+
catch { }
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
else {
|
|
1418
|
+
refs.window.hidden = true;
|
|
1419
|
+
}
|
|
1420
|
+
refs.wasVisible = false;
|
|
1421
|
+
refs.currentPosition = position;
|
|
963
1422
|
return true;
|
|
1423
|
+
}
|
|
1424
|
+
refs.window.hidden = false;
|
|
964
1425
|
refs.titleLabel.text = snapshot.title || '';
|
|
965
1426
|
refs.statusLabel.text = formatStatusText(snapshot);
|
|
966
1427
|
const host = getIosOverlayHost();
|
|
@@ -974,9 +1435,6 @@ function applySnapshotToIosRefs(refs, snapshot) {
|
|
|
974
1435
|
refs.panel.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(1, 0.96, 0.96, 1);
|
|
975
1436
|
refs.titleLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.7, 0.1, 0.06, 1);
|
|
976
1437
|
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
1438
|
}
|
|
981
1439
|
else if (isSuccess) {
|
|
982
1440
|
// Slightly more saturated green panel + dark-green
|
|
@@ -988,24 +1446,52 @@ function applySnapshotToIosRefs(refs, snapshot) {
|
|
|
988
1446
|
refs.panel.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0.9, 0.97, 0.91, 1);
|
|
989
1447
|
refs.titleLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.05, 0.43, 0.18, 1);
|
|
990
1448
|
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
1449
|
}
|
|
998
1450
|
else {
|
|
999
1451
|
// Default (info / warn) — existing connection look.
|
|
1000
1452
|
refs.panel.backgroundColor = UIColor.whiteColor;
|
|
1001
1453
|
refs.titleLabel.textColor = UIColor.blackColor;
|
|
1002
1454
|
refs.statusLabel.textColor = UIColor.darkGrayColor;
|
|
1455
|
+
}
|
|
1456
|
+
// Backdrop dims only in centered mode; toast mode keeps
|
|
1457
|
+
// the rest of the app fully visible/usable. Errors get
|
|
1458
|
+
// a slightly stronger dim in centered mode because the
|
|
1459
|
+
// user MUST notice them.
|
|
1460
|
+
if (isToast) {
|
|
1461
|
+
refs.backdrop.backgroundColor = UIColor.clearColor;
|
|
1462
|
+
}
|
|
1463
|
+
else if (isError) {
|
|
1464
|
+
refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0, 0, 0.35);
|
|
1465
|
+
}
|
|
1466
|
+
else if (isSuccess) {
|
|
1467
|
+
refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0.15, 0.05, 0.28);
|
|
1468
|
+
}
|
|
1469
|
+
else {
|
|
1003
1470
|
refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0, 0, 0.35);
|
|
1004
1471
|
}
|
|
1005
1472
|
}
|
|
1006
1473
|
catch { }
|
|
1007
1474
|
}
|
|
1008
|
-
layoutIosOverlayRefs(refs);
|
|
1475
|
+
const layout = layoutIosOverlayRefs(refs, position);
|
|
1476
|
+
// Slide-in animation only fires on the actual hidden→visible
|
|
1477
|
+
// transition (or on a position swap — e.g. dev toggling top
|
|
1478
|
+
// to bottom mid-cycle). Subsequent updates within the same
|
|
1479
|
+
// visible cycle just refresh text/colours without re-animating.
|
|
1480
|
+
const positionChanged = previousPosition !== position;
|
|
1481
|
+
const justAppeared = !wasVisible || positionChanged;
|
|
1482
|
+
if (justAppeared && isToast && layout) {
|
|
1483
|
+
animateIosPanelIn(refs, position, layout);
|
|
1484
|
+
}
|
|
1485
|
+
else if (justAppeared && !isToast) {
|
|
1486
|
+
// Centered modal: ensure alpha is reset to 1 in case a
|
|
1487
|
+
// previous toast-mode dismissal left it at 0.
|
|
1488
|
+
try {
|
|
1489
|
+
refs.panel.alpha = 1;
|
|
1490
|
+
}
|
|
1491
|
+
catch { }
|
|
1492
|
+
}
|
|
1493
|
+
refs.wasVisible = true;
|
|
1494
|
+
refs.currentPosition = position;
|
|
1009
1495
|
return true;
|
|
1010
1496
|
}
|
|
1011
1497
|
catch (err) {
|
|
@@ -1143,7 +1629,17 @@ function createOverlayApi() {
|
|
|
1143
1629
|
const state = getRuntimeState();
|
|
1144
1630
|
clearUpdateAutoHideTimer(state);
|
|
1145
1631
|
state.updateCycleStartedAt = 0;
|
|
1146
|
-
|
|
1632
|
+
const next = createBootOverlaySnapshot(stage, info);
|
|
1633
|
+
// Monotonic boot-progress ratchet: boot stages can fire out of
|
|
1634
|
+
// order across boot paths (native `__nsStartDevSession` vs the
|
|
1635
|
+
// http-bootloader fallback) and individual bases were tuned
|
|
1636
|
+
// independently, so clamp boot→boot transitions to never go
|
|
1637
|
+
// backwards. Non-boot snapshots (error/ready) bypass — they
|
|
1638
|
+
// genuinely want to reset the visual.
|
|
1639
|
+
if (next.mode === 'boot' && state.snapshot.mode === 'boot' && typeof next.progress === 'number' && typeof state.snapshot.progress === 'number' && next.progress < state.snapshot.progress) {
|
|
1640
|
+
next.progress = state.snapshot.progress;
|
|
1641
|
+
}
|
|
1642
|
+
return applyRuntimeSnapshot(next);
|
|
1147
1643
|
},
|
|
1148
1644
|
setConnectionStage(stage, info) {
|
|
1149
1645
|
const state = getRuntimeState();
|