@nativescript/vite 8.0.0-alpha.4 → 8.0.0-alpha.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. package/README.md +51 -11
  2. package/bin/cli.cjs +8 -14
  3. package/configuration/angular.d.ts +34 -1
  4. package/configuration/angular.js +376 -165
  5. package/configuration/angular.js.map +1 -1
  6. package/configuration/base.js +256 -30
  7. package/configuration/base.js.map +1 -1
  8. package/configuration/javascript.js +12 -90
  9. package/configuration/javascript.js.map +1 -1
  10. package/configuration/solid.js +33 -4
  11. package/configuration/solid.js.map +1 -1
  12. package/configuration/typescript.js +10 -90
  13. package/configuration/typescript.js.map +1 -1
  14. package/helpers/app-components.d.ts +2 -1
  15. package/helpers/app-components.js.map +1 -1
  16. package/helpers/app-css-state.d.ts +8 -0
  17. package/helpers/app-css-state.js +8 -0
  18. package/helpers/app-css-state.js.map +1 -0
  19. package/helpers/bundler-context.d.ts +11 -0
  20. package/helpers/bundler-context.js +71 -0
  21. package/helpers/bundler-context.js.map +1 -0
  22. package/helpers/config-as-json.js +10 -0
  23. package/helpers/config-as-json.js.map +1 -1
  24. package/helpers/css-platform-plugin.d.ts +14 -0
  25. package/helpers/css-platform-plugin.js +43 -25
  26. package/helpers/css-platform-plugin.js.map +1 -1
  27. package/helpers/dev-host.d.ts +360 -0
  28. package/helpers/dev-host.js +692 -0
  29. package/helpers/dev-host.js.map +1 -0
  30. package/helpers/dynamic-import-plugin.js +1 -1
  31. package/helpers/dynamic-import-plugin.js.map +1 -1
  32. package/helpers/esbuild-platform-resolver.js +4 -1
  33. package/helpers/esbuild-platform-resolver.js.map +1 -1
  34. package/helpers/external-configs.d.ts +10 -12
  35. package/helpers/external-configs.js +54 -35
  36. package/helpers/external-configs.js.map +1 -1
  37. package/helpers/global-defines.d.ts +128 -0
  38. package/helpers/global-defines.js +174 -11
  39. package/helpers/global-defines.js.map +1 -1
  40. package/helpers/hmr-scope.d.ts +26 -0
  41. package/helpers/hmr-scope.js +67 -0
  42. package/helpers/hmr-scope.js.map +1 -0
  43. package/helpers/init.js +0 -18
  44. package/helpers/init.js.map +1 -1
  45. package/helpers/logging.d.ts +1 -0
  46. package/helpers/logging.js +65 -4
  47. package/helpers/logging.js.map +1 -1
  48. package/helpers/main-entry.d.ts +3 -1
  49. package/helpers/main-entry.js +444 -50
  50. package/helpers/main-entry.js.map +1 -1
  51. package/helpers/nativeclass-esbuild-plugin.d.ts +2 -1
  52. package/helpers/nativeclass-esbuild-plugin.js.map +1 -1
  53. package/helpers/nativeclass-transform.js +5 -6
  54. package/helpers/nativeclass-transform.js.map +1 -1
  55. package/helpers/nativeclass-transformer-plugin.d.ts +9 -2
  56. package/helpers/nativeclass-transformer-plugin.js +157 -14
  57. package/helpers/nativeclass-transformer-plugin.js.map +1 -1
  58. package/helpers/nativescript-package-resolver.js +10 -62
  59. package/helpers/nativescript-package-resolver.js.map +1 -1
  60. package/helpers/normalize-id.d.ts +42 -0
  61. package/helpers/normalize-id.js +60 -0
  62. package/helpers/normalize-id.js.map +1 -0
  63. package/helpers/ns-core-url.d.ts +106 -0
  64. package/helpers/ns-core-url.js +225 -0
  65. package/helpers/ns-core-url.js.map +1 -0
  66. package/helpers/optimize-deps.d.ts +16 -0
  67. package/helpers/optimize-deps.js +17 -0
  68. package/helpers/optimize-deps.js.map +1 -0
  69. package/helpers/package-platform-aliases.js +34 -49
  70. package/helpers/package-platform-aliases.js.map +1 -1
  71. package/helpers/platform-types.d.ts +2 -0
  72. package/helpers/platform-types.js +2 -0
  73. package/helpers/platform-types.js.map +1 -0
  74. package/helpers/postcss-platform-config.d.ts +17 -1
  75. package/helpers/postcss-platform-config.js +20 -37
  76. package/helpers/postcss-platform-config.js.map +1 -1
  77. package/helpers/project.d.ts +35 -0
  78. package/helpers/project.js +120 -2
  79. package/helpers/project.js.map +1 -1
  80. package/helpers/resolve-main-field-platform.d.ts +20 -0
  81. package/helpers/resolve-main-field-platform.js +49 -0
  82. package/helpers/resolve-main-field-platform.js.map +1 -0
  83. package/helpers/resolver.js +17 -2
  84. package/helpers/resolver.js.map +1 -1
  85. package/helpers/theme-core-plugins.js +1 -1
  86. package/helpers/theme-core-plugins.js.map +1 -1
  87. package/helpers/ts-config-paths.d.ts +14 -0
  88. package/helpers/ts-config-paths.js +90 -9
  89. package/helpers/ts-config-paths.js.map +1 -1
  90. package/helpers/typescript-check.d.ts +2 -1
  91. package/helpers/typescript-check.js.map +1 -1
  92. package/helpers/ui-registration.d.ts +21 -0
  93. package/helpers/ui-registration.js +156 -0
  94. package/helpers/ui-registration.js.map +1 -0
  95. package/helpers/utils.js +1 -2
  96. package/helpers/utils.js.map +1 -1
  97. package/helpers/workers.d.ts +20 -19
  98. package/helpers/workers.js +624 -4
  99. package/helpers/workers.js.map +1 -1
  100. package/hmr/client/css-handler.d.ts +2 -1
  101. package/hmr/client/css-handler.js +50 -26
  102. package/hmr/client/css-handler.js.map +1 -1
  103. package/hmr/client/css-update-overlay.d.ts +18 -0
  104. package/hmr/client/css-update-overlay.js +27 -0
  105. package/hmr/client/css-update-overlay.js.map +1 -0
  106. package/hmr/client/framework-client-strategy.d.ts +79 -0
  107. package/hmr/client/framework-client-strategy.js +19 -0
  108. package/hmr/client/framework-client-strategy.js.map +1 -0
  109. package/hmr/client/hmr-pending-overlay.d.ts +13 -0
  110. package/hmr/client/hmr-pending-overlay.js +60 -0
  111. package/hmr/client/hmr-pending-overlay.js.map +1 -0
  112. package/hmr/client/index.js +820 -212
  113. package/hmr/client/index.js.map +1 -1
  114. package/hmr/client/utils.d.ts +7 -1
  115. package/hmr/client/utils.js +207 -29
  116. package/hmr/client/utils.js.map +1 -1
  117. package/hmr/entry-runtime.d.ts +2 -1
  118. package/hmr/entry-runtime.js +256 -69
  119. package/hmr/entry-runtime.js.map +1 -1
  120. package/hmr/frameworks/angular/build/angular-linker.d.ts +12 -0
  121. package/hmr/frameworks/angular/build/angular-linker.js +109 -0
  122. package/hmr/frameworks/angular/build/angular-linker.js.map +1 -0
  123. package/hmr/frameworks/angular/build/inject-component-hmr-registration.d.ts +112 -0
  124. package/hmr/frameworks/angular/build/inject-component-hmr-registration.js +291 -0
  125. package/hmr/frameworks/angular/build/inject-component-hmr-registration.js.map +1 -0
  126. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.d.ts +75 -0
  127. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.js +221 -0
  128. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.js.map +1 -0
  129. package/{helpers/angular → hmr/frameworks/angular/build}/inline-decorator-component-templates.js +1 -170
  130. package/hmr/frameworks/angular/build/inline-decorator-component-templates.js.map +1 -0
  131. package/hmr/frameworks/angular/build/js-lexer.d.ts +4 -0
  132. package/{helpers/angular/synthesize-decorator-ctor-parameters.js → hmr/frameworks/angular/build/js-lexer.js} +22 -96
  133. package/hmr/frameworks/angular/build/js-lexer.js.map +1 -0
  134. package/hmr/frameworks/angular/build/shared-linker.d.ts +39 -0
  135. package/hmr/frameworks/angular/build/shared-linker.js +128 -0
  136. package/hmr/frameworks/angular/build/shared-linker.js.map +1 -0
  137. package/hmr/frameworks/angular/build/synthesize-decorator-ctor-parameters.js +88 -0
  138. package/hmr/frameworks/angular/build/synthesize-decorator-ctor-parameters.js.map +1 -0
  139. package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-injectable-factories.js +1 -174
  140. package/hmr/frameworks/angular/build/synthesize-injectable-factories.js.map +1 -0
  141. package/{helpers/angular → hmr/frameworks/angular/build}/util.d.ts +1 -0
  142. package/hmr/frameworks/angular/build/util.js +155 -0
  143. package/hmr/frameworks/angular/build/util.js.map +1 -0
  144. package/hmr/frameworks/angular/client/index.d.ts +1 -0
  145. package/hmr/frameworks/angular/client/index.js +803 -21
  146. package/hmr/frameworks/angular/client/index.js.map +1 -1
  147. package/hmr/frameworks/angular/client/strategy.d.ts +9 -0
  148. package/hmr/frameworks/angular/client/strategy.js +19 -0
  149. package/hmr/frameworks/angular/client/strategy.js.map +1 -0
  150. package/hmr/frameworks/angular/server/angular-root-component.d.ts +79 -0
  151. package/hmr/frameworks/angular/server/angular-root-component.js +149 -0
  152. package/hmr/frameworks/angular/server/angular-root-component.js.map +1 -0
  153. package/hmr/frameworks/angular/server/linker.js +1 -4
  154. package/hmr/frameworks/angular/server/linker.js.map +1 -1
  155. package/hmr/frameworks/angular/server/strategy.js +460 -12
  156. package/hmr/frameworks/angular/server/strategy.js.map +1 -1
  157. package/hmr/{server → frameworks/angular/server}/websocket-angular-entry.js +2 -2
  158. package/hmr/frameworks/angular/server/websocket-angular-entry.js.map +1 -0
  159. package/hmr/{server → frameworks/angular/server}/websocket-angular-hot-update.d.ts +17 -11
  160. package/hmr/frameworks/angular/server/websocket-angular-hot-update.js +336 -0
  161. package/hmr/frameworks/angular/server/websocket-angular-hot-update.js.map +1 -0
  162. package/hmr/frameworks/solid/build/solid-jsx-deps.d.ts +15 -0
  163. package/hmr/frameworks/solid/build/solid-jsx-deps.js +178 -0
  164. package/hmr/frameworks/solid/build/solid-jsx-deps.js.map +1 -0
  165. package/hmr/frameworks/solid/server/strategy.js +291 -16
  166. package/hmr/frameworks/solid/server/strategy.js.map +1 -1
  167. package/hmr/frameworks/typescript/server/strategy.js +38 -14
  168. package/hmr/frameworks/typescript/server/strategy.js.map +1 -1
  169. package/hmr/frameworks/vue/client/dep-propagation.d.ts +36 -0
  170. package/hmr/frameworks/vue/client/dep-propagation.js +101 -0
  171. package/hmr/frameworks/vue/client/dep-propagation.js.map +1 -0
  172. package/hmr/frameworks/vue/client/index.d.ts +8 -0
  173. package/hmr/frameworks/vue/client/index.js +56 -243
  174. package/hmr/frameworks/vue/client/index.js.map +1 -1
  175. package/hmr/frameworks/vue/client/strategy.d.ts +33 -0
  176. package/hmr/frameworks/vue/client/strategy.js +157 -0
  177. package/hmr/frameworks/vue/client/strategy.js.map +1 -0
  178. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.d.ts +49 -0
  179. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.js +142 -0
  180. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.js.map +1 -0
  181. package/hmr/frameworks/vue/server/sfc-route-assemble.d.ts +7 -0
  182. package/hmr/frameworks/vue/server/sfc-route-assemble.js +747 -0
  183. package/hmr/frameworks/vue/server/sfc-route-assemble.js.map +1 -0
  184. package/hmr/frameworks/vue/server/sfc-route-meta.d.ts +7 -0
  185. package/hmr/frameworks/vue/server/sfc-route-meta.js +80 -0
  186. package/hmr/frameworks/vue/server/sfc-route-meta.js.map +1 -0
  187. package/hmr/frameworks/vue/server/sfc-route-serve.d.ts +8 -0
  188. package/hmr/frameworks/vue/server/sfc-route-serve.js +459 -0
  189. package/hmr/frameworks/vue/server/sfc-route-serve.js.map +1 -0
  190. package/hmr/frameworks/vue/server/sfc-route-shared.d.ts +38 -0
  191. package/hmr/frameworks/vue/server/sfc-route-shared.js +48 -0
  192. package/hmr/frameworks/vue/server/sfc-route-shared.js.map +1 -0
  193. package/hmr/frameworks/vue/server/strategy.d.ts +35 -0
  194. package/hmr/frameworks/vue/server/strategy.js +278 -1
  195. package/hmr/frameworks/vue/server/strategy.js.map +1 -1
  196. package/hmr/frameworks/vue/server/websocket-sfc.d.ts +15 -0
  197. package/hmr/frameworks/vue/server/websocket-sfc.js +20 -0
  198. package/hmr/frameworks/vue/server/websocket-sfc.js.map +1 -0
  199. package/hmr/helpers/ast-normalizer.d.ts +3 -1
  200. package/hmr/helpers/ast-normalizer.js +77 -10
  201. package/hmr/helpers/ast-normalizer.js.map +1 -1
  202. package/hmr/helpers/cjs-named-exports.d.ts +23 -0
  203. package/hmr/helpers/cjs-named-exports.js +152 -0
  204. package/hmr/helpers/cjs-named-exports.js.map +1 -0
  205. package/hmr/helpers/package-exports.d.ts +16 -0
  206. package/hmr/helpers/package-exports.js +396 -0
  207. package/hmr/helpers/package-exports.js.map +1 -0
  208. package/hmr/server/constants.js +20 -5
  209. package/hmr/server/constants.js.map +1 -1
  210. package/hmr/server/core-sanitize.d.ts +90 -7
  211. package/hmr/server/core-sanitize.js +211 -56
  212. package/hmr/server/core-sanitize.js.map +1 -1
  213. package/hmr/server/device-transform-helpers.d.ts +24 -0
  214. package/hmr/server/device-transform-helpers.js +369 -0
  215. package/hmr/server/device-transform-helpers.js.map +1 -0
  216. package/hmr/server/framework-strategy.d.ts +108 -11
  217. package/hmr/server/hmr-module-graph.d.ts +37 -0
  218. package/hmr/server/hmr-module-graph.js +214 -0
  219. package/hmr/server/hmr-module-graph.js.map +1 -0
  220. package/hmr/server/import-map.d.ts +11 -2
  221. package/hmr/server/import-map.js +95 -44
  222. package/hmr/server/import-map.js.map +1 -1
  223. package/hmr/server/index.js +7 -16
  224. package/hmr/server/index.js.map +1 -1
  225. package/hmr/server/ns-core-cjs-shape.d.ts +204 -0
  226. package/hmr/server/ns-core-cjs-shape.js +271 -0
  227. package/hmr/server/ns-core-cjs-shape.js.map +1 -0
  228. package/hmr/server/ns-rt-bridge.d.ts +51 -0
  229. package/hmr/server/ns-rt-bridge.js +131 -0
  230. package/hmr/server/ns-rt-bridge.js.map +1 -0
  231. package/hmr/server/ns-rt-route.d.ts +5 -0
  232. package/hmr/server/ns-rt-route.js +38 -0
  233. package/hmr/server/ns-rt-route.js.map +1 -0
  234. package/hmr/server/perf-instrumentation.d.ts +114 -0
  235. package/hmr/server/perf-instrumentation.js +197 -0
  236. package/hmr/server/perf-instrumentation.js.map +1 -0
  237. package/hmr/server/process-code-for-device.d.ts +14 -0
  238. package/hmr/server/process-code-for-device.js +702 -0
  239. package/hmr/server/process-code-for-device.js.map +1 -0
  240. package/hmr/server/require-guard.d.ts +1 -0
  241. package/hmr/server/require-guard.js +12 -0
  242. package/hmr/server/require-guard.js.map +1 -0
  243. package/hmr/server/rewrite-imports.d.ts +2 -0
  244. package/hmr/server/rewrite-imports.js +600 -0
  245. package/hmr/server/rewrite-imports.js.map +1 -0
  246. package/hmr/server/route-helpers.d.ts +7 -0
  247. package/hmr/server/route-helpers.js +13 -0
  248. package/hmr/server/route-helpers.js.map +1 -0
  249. package/hmr/server/server-origin.d.ts +2 -0
  250. package/hmr/server/server-origin.js +83 -0
  251. package/hmr/server/server-origin.js.map +1 -0
  252. package/hmr/server/shared-transform-request.js +13 -7
  253. package/hmr/server/shared-transform-request.js.map +1 -1
  254. package/hmr/server/transform-cache-invalidation.d.ts +37 -0
  255. package/hmr/server/transform-cache-invalidation.js +156 -0
  256. package/hmr/server/transform-cache-invalidation.js.map +1 -0
  257. package/hmr/server/vendor-bare-module-shims.d.ts +4 -0
  258. package/hmr/server/vendor-bare-module-shims.js +80 -0
  259. package/hmr/server/vendor-bare-module-shims.js.map +1 -0
  260. package/hmr/server/vite-plugin.js +72 -42
  261. package/hmr/server/vite-plugin.js.map +1 -1
  262. package/hmr/server/websocket-core-bridge.d.ts +45 -6
  263. package/hmr/server/websocket-core-bridge.js +81 -77
  264. package/hmr/server/websocket-core-bridge.js.map +1 -1
  265. package/hmr/server/websocket-css-hot-update.d.ts +33 -0
  266. package/hmr/server/websocket-css-hot-update.js +65 -0
  267. package/hmr/server/websocket-css-hot-update.js.map +1 -0
  268. package/hmr/server/websocket-device-transform.d.ts +3 -0
  269. package/hmr/server/websocket-device-transform.js +7 -0
  270. package/hmr/server/websocket-device-transform.js.map +1 -0
  271. package/hmr/server/websocket-graph-upsert.d.ts +15 -0
  272. package/hmr/server/websocket-graph-upsert.js +20 -0
  273. package/hmr/server/websocket-graph-upsert.js.map +1 -1
  274. package/hmr/server/websocket-hmr-pending.d.ts +37 -0
  275. package/hmr/server/websocket-hmr-pending.js +55 -0
  276. package/hmr/server/websocket-hmr-pending.js.map +1 -0
  277. package/hmr/server/websocket-hot-update.d.ts +77 -0
  278. package/hmr/server/websocket-hot-update.js +360 -0
  279. package/hmr/server/websocket-hot-update.js.map +1 -0
  280. package/hmr/server/websocket-import-map-route.d.ts +15 -0
  281. package/hmr/server/websocket-import-map-route.js +50 -0
  282. package/hmr/server/websocket-import-map-route.js.map +1 -0
  283. package/hmr/server/websocket-module-bindings.js +3 -3
  284. package/hmr/server/websocket-module-bindings.js.map +1 -1
  285. package/hmr/server/websocket-module-specifiers.d.ts +66 -2
  286. package/hmr/server/websocket-module-specifiers.js +203 -20
  287. package/hmr/server/websocket-module-specifiers.js.map +1 -1
  288. package/hmr/server/websocket-ns-core.d.ts +21 -0
  289. package/hmr/server/websocket-ns-core.js +311 -0
  290. package/hmr/server/websocket-ns-core.js.map +1 -0
  291. package/hmr/server/websocket-ns-entry.d.ts +21 -0
  292. package/hmr/server/websocket-ns-entry.js +164 -0
  293. package/hmr/server/websocket-ns-entry.js.map +1 -0
  294. package/hmr/server/websocket-ns-m-paths.d.ts +1 -1
  295. package/hmr/server/websocket-ns-m-paths.js +58 -13
  296. package/hmr/server/websocket-ns-m-paths.js.map +1 -1
  297. package/hmr/server/websocket-ns-m-request.d.ts +11 -1
  298. package/hmr/server/websocket-ns-m-request.js +18 -25
  299. package/hmr/server/websocket-ns-m-request.js.map +1 -1
  300. package/hmr/server/websocket-ns-m.d.ts +33 -0
  301. package/hmr/server/websocket-ns-m.js +752 -0
  302. package/hmr/server/websocket-ns-m.js.map +1 -0
  303. package/hmr/server/websocket-served-module-helpers.d.ts +82 -0
  304. package/hmr/server/websocket-served-module-helpers.js +879 -0
  305. package/hmr/server/websocket-served-module-helpers.js.map +1 -0
  306. package/hmr/server/websocket-txn.js +2 -8
  307. package/hmr/server/websocket-txn.js.map +1 -1
  308. package/hmr/server/websocket-vendor-unifier.d.ts +0 -1
  309. package/hmr/server/websocket-vendor-unifier.js +4 -9
  310. package/hmr/server/websocket-vendor-unifier.js.map +1 -1
  311. package/hmr/server/websocket.d.ts +8 -39
  312. package/hmr/server/websocket.js +514 -4030
  313. package/hmr/server/websocket.js.map +1 -1
  314. package/hmr/shared/ns-globals.d.ts +118 -0
  315. package/hmr/shared/ns-globals.js +29 -0
  316. package/hmr/shared/ns-globals.js.map +1 -0
  317. package/hmr/shared/protocol.d.ts +145 -0
  318. package/hmr/shared/protocol.js +28 -0
  319. package/hmr/shared/protocol.js.map +1 -0
  320. package/hmr/shared/runtime/boot-placeholder-ui.d.ts +69 -0
  321. package/hmr/shared/runtime/boot-placeholder-ui.js +101 -0
  322. package/hmr/shared/runtime/boot-placeholder-ui.js.map +1 -0
  323. package/hmr/shared/runtime/boot-progress.d.ts +44 -0
  324. package/hmr/shared/runtime/boot-progress.js +133 -0
  325. package/hmr/shared/runtime/boot-progress.js.map +1 -0
  326. package/hmr/shared/runtime/boot-timeline.d.ts +18 -0
  327. package/hmr/shared/runtime/boot-timeline.js +42 -0
  328. package/hmr/shared/runtime/boot-timeline.js.map +1 -0
  329. package/hmr/shared/runtime/browser-runtime-contract.js.map +1 -1
  330. package/hmr/shared/runtime/dev-overlay-snapshots.d.ts +31 -0
  331. package/hmr/shared/runtime/dev-overlay-snapshots.js +324 -0
  332. package/hmr/shared/runtime/dev-overlay-snapshots.js.map +1 -0
  333. package/hmr/shared/runtime/dev-overlay.d.ts +119 -26
  334. package/hmr/shared/runtime/dev-overlay.js +1153 -261
  335. package/hmr/shared/runtime/dev-overlay.js.map +1 -1
  336. package/hmr/shared/runtime/global-scope.d.ts +18 -0
  337. package/hmr/shared/runtime/global-scope.js +21 -0
  338. package/hmr/shared/runtime/global-scope.js.map +1 -0
  339. package/hmr/shared/runtime/hooks.js +2 -1
  340. package/hmr/shared/runtime/hooks.js.map +1 -1
  341. package/hmr/shared/runtime/http-only-boot.js +7 -6
  342. package/hmr/shared/runtime/http-only-boot.js.map +1 -1
  343. package/hmr/shared/runtime/module-provenance.js +4 -6
  344. package/hmr/shared/runtime/module-provenance.js.map +1 -1
  345. package/hmr/shared/runtime/root-placeholder-view.d.ts +19 -0
  346. package/hmr/shared/runtime/root-placeholder-view.js +311 -0
  347. package/hmr/shared/runtime/root-placeholder-view.js.map +1 -0
  348. package/hmr/shared/runtime/root-placeholder.js +371 -197
  349. package/hmr/shared/runtime/root-placeholder.js.map +1 -1
  350. package/hmr/shared/runtime/session-bootstrap.js +168 -4
  351. package/hmr/shared/runtime/session-bootstrap.js.map +1 -1
  352. package/hmr/shared/runtime/vendor-bootstrap.js +3 -10
  353. package/hmr/shared/runtime/vendor-bootstrap.js.map +1 -1
  354. package/hmr/shared/vendor/manifest-collect.d.ts +4 -0
  355. package/hmr/shared/vendor/manifest-collect.js +512 -0
  356. package/hmr/shared/vendor/manifest-collect.js.map +1 -0
  357. package/hmr/shared/vendor/manifest-loader.d.ts +2 -1
  358. package/hmr/shared/vendor/manifest-loader.js +5 -3
  359. package/hmr/shared/vendor/manifest-loader.js.map +1 -1
  360. package/hmr/shared/vendor/manifest.d.ts +1 -7
  361. package/hmr/shared/vendor/manifest.js +102 -741
  362. package/hmr/shared/vendor/manifest.js.map +1 -1
  363. package/hmr/shared/vendor/vendor-device-shim.d.ts +1 -0
  364. package/hmr/shared/vendor/vendor-device-shim.js +208 -0
  365. package/hmr/shared/vendor/vendor-device-shim.js.map +1 -0
  366. package/hmr/shared/vendor/vendor-esbuild-plugins.d.ts +16 -0
  367. package/hmr/shared/vendor/vendor-esbuild-plugins.js +203 -0
  368. package/hmr/shared/vendor/vendor-esbuild-plugins.js.map +1 -0
  369. package/hmr/vendor-bootstrap.d.ts +1 -3
  370. package/hmr/vendor-bootstrap.js +4 -6
  371. package/hmr/vendor-bootstrap.js.map +1 -1
  372. package/index.d.ts +1 -0
  373. package/index.js +5 -0
  374. package/index.js.map +1 -1
  375. package/package.json +55 -11
  376. package/runtime/core-aliases-early.js +25 -55
  377. package/runtime/core-aliases-early.js.map +1 -1
  378. package/helpers/angular/angular-linker.d.ts +0 -13
  379. package/helpers/angular/angular-linker.js +0 -194
  380. package/helpers/angular/angular-linker.js.map +0 -1
  381. package/helpers/angular/inline-decorator-component-templates.js.map +0 -1
  382. package/helpers/angular/shared-linker.d.ts +0 -11
  383. package/helpers/angular/shared-linker.js +0 -75
  384. package/helpers/angular/shared-linker.js.map +0 -1
  385. package/helpers/angular/synthesize-decorator-ctor-parameters.js.map +0 -1
  386. package/helpers/angular/synthesize-injectable-factories.js.map +0 -1
  387. package/helpers/angular/util.js +0 -67
  388. package/helpers/angular/util.js.map +0 -1
  389. package/helpers/prelink-angular.d.ts +0 -2
  390. package/helpers/prelink-angular.js +0 -117
  391. package/helpers/prelink-angular.js.map +0 -1
  392. package/hmr/server/websocket-angular-entry.js.map +0 -1
  393. package/hmr/server/websocket-angular-hot-update.js +0 -239
  394. package/hmr/server/websocket-angular-hot-update.js.map +0 -1
  395. package/hmr/server/websocket-ns-m-finalize.d.ts +0 -32
  396. package/hmr/server/websocket-ns-m-finalize.js +0 -73
  397. package/hmr/server/websocket-ns-m-finalize.js.map +0 -1
  398. package/hmr/server/websocket-runtime-compat.d.ts +0 -19
  399. package/hmr/server/websocket-runtime-compat.js +0 -286
  400. package/hmr/server/websocket-runtime-compat.js.map +0 -1
  401. package/hmr/server/websocket-vue-sfc.d.ts +0 -35
  402. package/hmr/server/websocket-vue-sfc.js +0 -1116
  403. package/hmr/server/websocket-vue-sfc.js.map +0 -1
  404. package/transformers/NativeClass/index.d.ts +0 -2
  405. package/transformers/NativeClass/index.js +0 -222
  406. package/transformers/NativeClass/index.js.map +0 -1
  407. /package/{helpers/angular → hmr/frameworks/angular/build}/inline-decorator-component-templates.d.ts +0 -0
  408. /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-decorator-ctor-parameters.d.ts +0 -0
  409. /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-injectable-factories.d.ts +0 -0
  410. /package/hmr/{server → frameworks/angular/server}/websocket-angular-entry.d.ts +0 -0
@@ -1,18 +1,42 @@
1
- const BOOT_TITLE = 'NativeScript Vite preparing dev session...';
2
- const DEFAULT_SNAPSHOT = {
3
- visible: false,
4
- mode: 'hidden',
5
- badge: 'HMR',
6
- title: BOOT_TITLE,
7
- phase: '',
8
- detail: '',
9
- progress: null,
10
- busy: false,
11
- blocking: false,
12
- tone: 'info',
13
- };
1
+ import { BOOT_PLACEHOLDER_MOTION, computeBootProgressFillScale, formatBootDetailLine, formatBootPrimaryLine } from './boot-placeholder-ui.js';
2
+ import { BOOT_TITLE, DEFAULT_SNAPSHOT, createBootOverlaySnapshot, createConnectionOverlaySnapshot, createUpdateOverlaySnapshot } from './dev-overlay-snapshots.js';
3
+ import { getGlobalScope } from './global-scope.js';
4
+ // Re-export the snapshot model so existing `./dev-overlay.js` importers keep working.
5
+ export { createBootOverlaySnapshot, createConnectionOverlaySnapshot, createUpdateOverlaySnapshot } from './dev-overlay-snapshots.js';
6
+ const DEFAULT_OVERLAY_POSITION = 'top';
14
7
  function getOverlayGlobal() {
15
- return globalThis;
8
+ return getGlobalScope();
9
+ }
10
+ /**
11
+ * Resolve the configured live-overlay position.
12
+ *
13
+ * Reads `globalThis.__NS_HMR_OVERLAY_POSITION__` so a project can
14
+ * override the default at boot time (e.g. inside `app.ts` before the
15
+ * Vite session bootstraps). Falls back to 'top' which gives the
16
+ * toast-style chip with a slide-in animation and safe-area padding.
17
+ */
18
+ export function getHmrDevOverlayPosition() {
19
+ const g = getOverlayGlobal();
20
+ const stored = g.__NS_HMR_OVERLAY_POSITION__;
21
+ if (stored === 'top' || stored === 'bottom' || stored === 'center') {
22
+ return stored;
23
+ }
24
+ return DEFAULT_OVERLAY_POSITION;
25
+ }
26
+ /**
27
+ * Imperative setter for the live-overlay position. Re-applies the
28
+ * current snapshot so the change is visible without waiting for the
29
+ * next HMR cycle. Useful during dev to A/B between top/bottom/center
30
+ * without restarting the app.
31
+ */
32
+ export function setHmrDevOverlayPosition(position) {
33
+ if (position !== 'top' && position !== 'bottom' && position !== 'center') {
34
+ return;
35
+ }
36
+ const g = getOverlayGlobal();
37
+ g.__NS_HMR_OVERLAY_POSITION__ = position;
38
+ const state = getRuntimeState();
39
+ applyRuntimeSnapshot(state.snapshot);
16
40
  }
17
41
  function getRuntimeState() {
18
42
  const g = getOverlayGlobal();
@@ -21,240 +45,25 @@ function getRuntimeState() {
21
45
  snapshot: { ...DEFAULT_SNAPSHOT },
22
46
  bootRefs: null,
23
47
  liveRefs: null,
48
+ iosRefs: null,
49
+ iosBuildFailed: false,
24
50
  verbose: false,
51
+ updateAutoHideTimer: null,
52
+ updateCycleStartedAt: 0,
25
53
  };
26
54
  }
27
- return g.__NS_HMR_DEV_OVERLAY_STATE__;
28
- }
29
- function describeAttempt(info) {
30
- const attempt = Number(info?.attempt || 0);
31
- const attempts = Number(info?.attempts || 0);
32
- if (!attempt || !attempts) {
33
- return '';
34
- }
35
- return `Attempt ${attempt}/${attempts}`;
36
- }
37
- export function createBootOverlaySnapshot(stage, info) {
38
- const attemptText = describeAttempt(info);
39
- const phaseInfo = {
40
- placeholder: {
41
- visible: true,
42
- mode: 'boot',
43
- badge: 'BOOT',
44
- title: BOOT_TITLE,
45
- phase: 'Preparing the HTTP HMR bootstrap',
46
- progress: 4,
47
- busy: true,
48
- blocking: true,
49
- tone: 'info',
50
- },
51
- 'probing-origin': {
52
- visible: true,
53
- mode: 'boot',
54
- badge: 'BOOT',
55
- title: BOOT_TITLE,
56
- phase: 'Contacting the Vite dev server',
57
- progress: 8,
58
- busy: true,
59
- blocking: true,
60
- tone: 'info',
61
- },
62
- 'loading-entry-runtime': {
63
- visible: true,
64
- mode: 'boot',
65
- badge: 'BOOT',
66
- title: BOOT_TITLE,
67
- phase: 'Loading the entry runtime bridge',
68
- progress: 16,
69
- busy: true,
70
- blocking: true,
71
- tone: 'info',
72
- },
73
- 'configuring-import-map': {
74
- visible: true,
75
- mode: 'boot',
76
- badge: 'BOOT',
77
- title: BOOT_TITLE,
78
- phase: 'Configuring the import map',
79
- progress: 26,
80
- busy: true,
81
- blocking: true,
82
- tone: 'info',
83
- },
84
- 'loading-runtime-bridge': {
85
- visible: true,
86
- mode: 'boot',
87
- badge: 'BOOT',
88
- title: BOOT_TITLE,
89
- phase: 'Loading the NativeScript runtime bridge',
90
- progress: 40,
91
- busy: true,
92
- blocking: true,
93
- tone: 'info',
94
- },
95
- 'loading-core-bridge': {
96
- visible: true,
97
- mode: 'boot',
98
- badge: 'BOOT',
99
- title: BOOT_TITLE,
100
- phase: 'Loading the unified core bridge',
101
- progress: 54,
102
- busy: true,
103
- blocking: true,
104
- tone: 'info',
105
- },
106
- 'preloading-style-scope': {
107
- visible: true,
108
- mode: 'boot',
109
- badge: 'BOOT',
110
- title: BOOT_TITLE,
111
- phase: 'Preparing the shared style scope',
112
- progress: 62,
113
- busy: true,
114
- blocking: true,
115
- tone: 'info',
116
- },
117
- 'installing-css': {
118
- visible: true,
119
- mode: 'boot',
120
- badge: 'BOOT',
121
- title: BOOT_TITLE,
122
- phase: 'Applying the app stylesheet',
123
- progress: 70,
124
- busy: true,
125
- blocking: true,
126
- tone: 'info',
127
- },
128
- 'importing-main': {
129
- visible: true,
130
- mode: 'boot',
131
- badge: 'BOOT',
132
- title: BOOT_TITLE,
133
- phase: 'Importing the app entry',
134
- progress: 82,
135
- busy: true,
136
- blocking: true,
137
- tone: 'info',
138
- },
139
- 'waiting-for-app': {
140
- visible: true,
141
- mode: 'boot',
142
- badge: 'BOOT',
143
- title: BOOT_TITLE,
144
- phase: 'Waiting for the app root view',
145
- progress: 94,
146
- busy: true,
147
- blocking: true,
148
- tone: 'info',
149
- },
150
- 'app-root-committed': {
151
- visible: true,
152
- mode: 'boot',
153
- badge: 'READY',
154
- title: BOOT_TITLE,
155
- phase: 'Real app root committed',
156
- progress: 100,
157
- busy: false,
158
- blocking: true,
159
- tone: 'info',
160
- },
161
- ready: {
162
- visible: true,
163
- mode: 'boot',
164
- badge: 'READY',
165
- title: BOOT_TITLE,
166
- phase: 'Boot complete',
167
- progress: 100,
168
- busy: false,
169
- blocking: true,
170
- tone: 'info',
171
- },
172
- error: {
173
- visible: true,
174
- mode: 'boot',
175
- badge: 'RETRY',
176
- title: BOOT_TITLE,
177
- phase: 'Retrying after a boot failure',
178
- progress: null,
179
- busy: true,
180
- blocking: true,
181
- tone: 'error',
182
- },
183
- };
184
- const base = phaseInfo[stage];
185
- let detail = info?.detail || '';
186
- if (!detail && stage === 'probing-origin' && info?.origin) {
187
- detail = `Trying ${info.origin}`;
188
- }
189
- if (attemptText) {
190
- detail = detail ? `${detail} ${attemptText}` : attemptText;
191
- }
192
- return {
193
- ...base,
194
- detail,
195
- progress: typeof info?.progress === 'number' || info?.progress === null ? info.progress : base.progress,
196
- };
197
- }
198
- export function createConnectionOverlaySnapshot(stage, info) {
199
- if (stage === 'healthy') {
200
- return { ...DEFAULT_SNAPSHOT };
201
- }
202
- const phaseInfo = {
203
- connecting: {
204
- visible: true,
205
- mode: 'connection',
206
- badge: 'SOCKET',
207
- title: 'Waiting for Vite dev server',
208
- phase: 'Opening websocket connection',
209
- detail: 'Live updates are paused until the connection is healthy.',
210
- progress: null,
211
- busy: true,
212
- blocking: false,
213
- tone: 'warn',
214
- },
215
- reconnecting: {
216
- visible: true,
217
- mode: 'connection',
218
- badge: 'SOCKET',
219
- title: 'HMR connection lost',
220
- phase: 'Reconnecting Vite websocket',
221
- detail: 'The app may be stale until the dev server reconnects.',
222
- progress: null,
223
- busy: true,
224
- blocking: false,
225
- tone: 'warn',
226
- },
227
- synchronizing: {
228
- visible: true,
229
- mode: 'connection',
230
- badge: 'SYNC',
231
- title: 'HMR connection restored',
232
- phase: 'Synchronizing live-update state',
233
- detail: 'Finalizing the module graph before dismissing the overlay.',
234
- progress: 95,
235
- busy: true,
236
- blocking: false,
237
- tone: 'info',
238
- },
239
- offline: {
240
- visible: true,
241
- mode: 'connection',
242
- badge: 'OFFLINE',
243
- title: 'Vite dev server offline',
244
- phase: 'Idle...',
245
- detail: 'The websocket and HTTP HMR path are both unavailable right now.',
246
- progress: null,
247
- busy: true,
248
- blocking: false,
249
- tone: 'error',
250
- },
251
- };
252
- const base = phaseInfo[stage];
253
- return {
254
- ...base,
255
- detail: info?.detail || base.detail,
256
- progress: typeof info?.progress === 'number' || info?.progress === null ? info.progress : base.progress,
257
- };
55
+ const state = g.__NS_HMR_DEV_OVERLAY_STATE__;
56
+ // Backfill newer fields for legacy state objects (e.g. after hot reload)
57
+ // so we never observe an undefined iosRefs/iosBuildFailed at runtime.
58
+ if (typeof state.iosRefs === 'undefined')
59
+ state.iosRefs = null;
60
+ if (typeof state.iosBuildFailed === 'undefined')
61
+ state.iosBuildFailed = false;
62
+ if (typeof state.updateAutoHideTimer === 'undefined')
63
+ state.updateAutoHideTimer = null;
64
+ if (typeof state.updateCycleStartedAt !== 'number')
65
+ state.updateCycleStartedAt = 0;
66
+ return state;
258
67
  }
259
68
  function resolveCoreExport(name) {
260
69
  const g = getOverlayGlobal();
@@ -435,10 +244,28 @@ function findBootStatusLabel() {
435
244
  catch { }
436
245
  return null;
437
246
  }
247
+ function findBootDetailLabel() {
248
+ const g = getOverlayGlobal();
249
+ return g.__NS_DEV_BOOT_DETAIL_LABEL__ || null;
250
+ }
251
+ function findBootProgressFill() {
252
+ const g = getOverlayGlobal();
253
+ return g.__NS_DEV_BOOT_PROGRESS_FILL__ || null;
254
+ }
438
255
  function updateBootStatusLabel(snapshot) {
439
- const newText = formatStatusText(snapshot) || 'Preparing the HTTP HMR bootstrap (4%)';
440
256
  const statusLabel = findBootStatusLabel();
257
+ const detailLabel = findBootDetailLabel();
258
+ const progressFill = findBootProgressFill();
441
259
  const activityIndicator = findBootActivityIndicator();
260
+ // New (card) layout: phase line + detail line live in separate
261
+ // labels so the typography can differ. Legacy (single-label)
262
+ // layout: keep the original combined "phase (X%)\ndetail" text so
263
+ // nothing visually regresses for runtimes still attached to the
264
+ // older placeholder shape.
265
+ const hasSplitLabels = !!detailLabel;
266
+ const phaseLine = formatBootPrimaryLine(snapshot);
267
+ const detailLine = formatBootDetailLine(snapshot);
268
+ const combinedText = formatStatusText(snapshot) || 'Preparing the HTTP HMR bootstrap (4%)';
442
269
  if (!statusLabel) {
443
270
  if (activityIndicator) {
444
271
  try {
@@ -447,11 +274,16 @@ function updateBootStatusLabel(snapshot) {
447
274
  }
448
275
  catch { }
449
276
  }
277
+ applyBootProgressFill(progressFill, snapshot);
450
278
  return;
451
279
  }
452
280
  try {
453
- statusLabel.text = newText;
454
- statusLabel.color = asColor(snapshot.tone === 'error' ? '#b41810e6' : '#563e3fb1');
281
+ statusLabel.text = hasSplitLabels ? phaseLine || 'Preparing the HTTP HMR bootstrap' : combinedText;
282
+ // Card layout uses the calibrated phase-text colour from the
283
+ // palette; legacy single-label layout keeps the original muted
284
+ // brown so we don't visually regress mid-session.
285
+ const phaseColorHex = snapshot.tone === 'error' ? '#B91C1C' : hasSplitLabels ? '#475569' : '#563e3fb1';
286
+ statusLabel.color = asColor(phaseColorHex);
455
287
  if (typeof statusLabel.requestLayout === 'function') {
456
288
  statusLabel.requestLayout();
457
289
  }
@@ -461,6 +293,15 @@ function updateBootStatusLabel(snapshot) {
461
293
  }
462
294
  }
463
295
  catch { }
296
+ if (detailLabel) {
297
+ try {
298
+ detailLabel.text = detailLine;
299
+ detailLabel.color = asColor(snapshot.tone === 'error' ? '#DC2626' : '#94A3B8');
300
+ detailLabel.visibility = detailLine ? 'visible' : 'collapse';
301
+ }
302
+ catch { }
303
+ }
304
+ applyBootProgressFill(progressFill, snapshot);
464
305
  if (activityIndicator) {
465
306
  try {
466
307
  activityIndicator.busy = !!snapshot.busy;
@@ -469,6 +310,50 @@ function updateBootStatusLabel(snapshot) {
469
310
  catch { }
470
311
  }
471
312
  }
313
+ // Drive the progress fill scaleX from the snapshot. Uses NS's view
314
+ // animate API for a smooth 220 ms easeOut between heartbeat ticks; a
315
+ // monotonic ratchet on `globalThis.__NS_DEV_BOOT_PROGRESS_LAST_SCALE__`
316
+ // guards against the fill snapping backwards if a less-progressed
317
+ // snapshot ever lands between ticks (mirrors the JS-side
318
+ // `applyMonotonicBootProgress` contract).
319
+ function applyBootProgressFill(progressFill, snapshot) {
320
+ if (!progressFill)
321
+ return;
322
+ const g = getOverlayGlobal();
323
+ const isError = snapshot.tone === 'error';
324
+ progressFill.backgroundColor = asColor(isError ? '#B41810' : '#3B6FE5');
325
+ const targetScale = computeBootProgressFillScale(snapshot.progress ?? null);
326
+ const previousRaw = Number(g.__NS_DEV_BOOT_PROGRESS_LAST_SCALE__);
327
+ const previous = Number.isFinite(previousRaw) ? previousRaw : 0;
328
+ const next = Math.max(previous, targetScale);
329
+ g.__NS_DEV_BOOT_PROGRESS_LAST_SCALE__ = next;
330
+ try {
331
+ // NS view.animate scales around `originX`/`originY`; the
332
+ // placeholder builder pins `originX = 0` so the fill grows
333
+ // rightward. animate() may be unavailable in some headless
334
+ // test environments — fall through to a direct property set.
335
+ if (typeof progressFill.animate === 'function') {
336
+ progressFill
337
+ .animate({
338
+ scale: { x: next, y: 1 },
339
+ duration: BOOT_PLACEHOLDER_MOTION.progressDurationMs,
340
+ curve: 'easeOut',
341
+ })
342
+ .catch(() => {
343
+ try {
344
+ progressFill.scaleX = next;
345
+ }
346
+ catch { }
347
+ });
348
+ }
349
+ else {
350
+ progressFill.scaleX = next;
351
+ }
352
+ }
353
+ catch {
354
+ progressFill.scaleX = next;
355
+ }
356
+ }
472
357
  function resolveActivePage() {
473
358
  try {
474
359
  const Frame = resolveCoreExport('Frame');
@@ -492,6 +377,87 @@ function resolveActivePage() {
492
377
  catch { }
493
378
  return null;
494
379
  }
380
+ function readCachedAndroidSafeAreaInsets() {
381
+ const g = getOverlayGlobal();
382
+ const cached = g.__NS_HMR_ANDROID_SAFE_INSETS__;
383
+ if (cached && typeof cached.top === 'number') {
384
+ return cached;
385
+ }
386
+ return null;
387
+ }
388
+ /**
389
+ * Read the Android system-bar safe-area insets (status bar / navigation
390
+ * bar / display cutout) in device-independent pixels.
391
+ *
392
+ * NativeScript runs every Android activity edge-to-edge, so the overlay
393
+ * wrapper fills the whole window — we need these insets to keep the toast
394
+ * chip out from under the system bars (see `computeAndroidToastMargin`).
395
+ *
396
+ * Returns null on non-Android runtimes (iOS / web / vitest) so callers
397
+ * fall back to base margins. The last good reading is cached on the
398
+ * global so a transient null (e.g. insets queried before the decor view
399
+ * is attached) reuses the previous value instead of snapping the chip
400
+ * back under the status bar.
401
+ */
402
+ function readAndroidSafeAreaInsets() {
403
+ const g = getOverlayGlobal();
404
+ // `android.*` is exposed as a global only on the NativeScript Android
405
+ // runtime; its absence means iOS, web, or a test environment.
406
+ const androidNs = g.android || (typeof globalThis.android !== 'undefined' ? globalThis.android : undefined);
407
+ if (!androidNs) {
408
+ return null;
409
+ }
410
+ try {
411
+ const Application = resolveCoreExport('Application');
412
+ const activity = Application?.android?.foregroundActivity || Application?.android?.startActivity;
413
+ const decorView = activity?.getWindow?.()?.getDecorView?.();
414
+ const rootInsets = decorView?.getRootWindowInsets?.();
415
+ if (!rootInsets) {
416
+ return readCachedAndroidSafeAreaInsets();
417
+ }
418
+ let density = 1;
419
+ try {
420
+ const metricsDensity = Number(activity?.getResources?.()?.getDisplayMetrics?.()?.density);
421
+ if (Number.isFinite(metricsDensity) && metricsDensity > 0) {
422
+ density = metricsDensity;
423
+ }
424
+ }
425
+ catch { }
426
+ let px = { top: 0, bottom: 0, left: 0, right: 0 };
427
+ const WindowInsets = androidNs.view?.WindowInsets;
428
+ const Type = WindowInsets?.Type;
429
+ if (typeof rootInsets.getInsets === 'function' && Type && typeof Type.systemBars === 'function') {
430
+ // API 30+: query the system bars + display cutout so notches /
431
+ // camera holes are respected in landscape.
432
+ let mask = Type.systemBars();
433
+ if (typeof Type.displayCutout === 'function') {
434
+ mask = mask | Type.displayCutout();
435
+ }
436
+ const i = rootInsets.getInsets(mask);
437
+ px = { top: Number(i?.top) || 0, bottom: Number(i?.bottom) || 0, left: Number(i?.left) || 0, right: Number(i?.right) || 0 };
438
+ }
439
+ else {
440
+ // API < 30 fallback.
441
+ px = {
442
+ top: Number(rootInsets.getSystemWindowInsetTop?.()) || 0,
443
+ bottom: Number(rootInsets.getSystemWindowInsetBottom?.()) || 0,
444
+ left: Number(rootInsets.getSystemWindowInsetLeft?.()) || 0,
445
+ right: Number(rootInsets.getSystemWindowInsetRight?.()) || 0,
446
+ };
447
+ }
448
+ const insets = {
449
+ top: px.top / density,
450
+ bottom: px.bottom / density,
451
+ left: px.left / density,
452
+ right: px.right / density,
453
+ };
454
+ g.__NS_HMR_ANDROID_SAFE_INSETS__ = insets;
455
+ return insets;
456
+ }
457
+ catch {
458
+ return readCachedAndroidSafeAreaInsets();
459
+ }
460
+ }
495
461
  function buildLiveOverlayView(snapshot) {
496
462
  const GridLayout = resolveCoreExport('GridLayout');
497
463
  const StackLayout = resolveCoreExport('StackLayout');
@@ -505,8 +471,18 @@ function buildLiveOverlayView(snapshot) {
505
471
  overlay.height = '100%';
506
472
  overlay.horizontalAlignment = 'stretch';
507
473
  overlay.verticalAlignment = 'stretch';
474
+ // Toast mode lets touches reach the underlying app. We flip
475
+ // isUserInteractionEnabled in applySnapshotToLiveRefs based on
476
+ // the resolved position, but keep it false here as a safe default
477
+ // (the panel itself is purely informational).
478
+ try {
479
+ overlay.isUserInteractionEnabled = false;
480
+ }
481
+ catch { }
508
482
  const panel = new StackLayout();
509
483
  panel.horizontalAlignment = 'center';
484
+ // Vertical alignment is overridden in applySnapshotToLiveRefs
485
+ // based on getHmrDevOverlayPosition(); 'middle' is the default
510
486
  panel.verticalAlignment = 'middle';
511
487
  panel.width = 320;
512
488
  panel.margin = 24;
@@ -528,6 +504,8 @@ function buildLiveOverlayView(snapshot) {
528
504
  overlay,
529
505
  titleLabel,
530
506
  statusLabel,
507
+ wasVisible: false,
508
+ currentPosition: getHmrDevOverlayPosition(),
531
509
  };
532
510
  applySnapshotToLiveRefs(refs, snapshot);
533
511
  return refs;
@@ -584,26 +562,777 @@ function applySnapshotToLiveRefs(refs, snapshot) {
584
562
  if (!refs) {
585
563
  return;
586
564
  }
587
- const visible = snapshot.visible && snapshot.mode === 'connection';
588
- refs.overlay.visibility = visible ? 'visible' : 'collapse';
589
- refs.overlay.backgroundColor = asColor(snapshot.tone === 'error' ? '#b4181068' : '#a1771683');
565
+ // 'update' mode shares the live (in-tree) overlay chrome with
566
+ // 'connection'. Both render a small panel inside the page; only
567
+ // the colours, text, and (now) panel position change with the
568
+ // snapshot's tone and the configured overlay position.
569
+ const visible = snapshot.visible && (snapshot.mode === 'connection' || snapshot.mode === 'update');
570
+ const wasVisible = !!refs.wasVisible;
571
+ const position = getHmrDevOverlayPosition();
572
+ const previousPosition = refs.currentPosition || position;
573
+ const isToast = position !== 'center';
590
574
  refs.titleLabel.text = snapshot.title;
591
- refs.titleLabel.color = asColor(snapshot.tone === 'error' ? '#b41810e6' : '#563e3fb1');
592
575
  refs.statusLabel.text = formatStatusText(snapshot);
593
- refs.statusLabel.color = asColor(snapshot.tone === 'error' ? '#b41810e6' : '#563e3fb1');
576
+ const textColor = snapshot.tone === 'error' ? '#b41810e6' : snapshot.tone === 'success' ? '#0e6e2fff' : '#563e3fb1';
577
+ refs.titleLabel.color = asColor(textColor);
578
+ refs.statusLabel.color = asColor(textColor);
579
+ // Backdrop tints (centered modal only). Toast modes use a fully
580
+ // transparent backdrop so the rest of the app stays visible AND
581
+ // reachable; the panel itself carries enough colour to stand out.
582
+ if (isToast) {
583
+ refs.overlay.backgroundColor = asColor('transparent');
584
+ }
585
+ else {
586
+ // Original wash-by-tone for centered:
587
+ // error → red wash (matches existing UX)
588
+ // success → richer green wash so the apply event is visible
589
+ // on bright app backgrounds
590
+ // default → warm orange (existing connection-overlay look)
591
+ const overlayBg = snapshot.tone === 'error' ? '#b4181068' : snapshot.tone === 'success' ? '#1f883d80' : '#a1771683';
592
+ refs.overlay.backgroundColor = asColor(overlayBg);
593
+ }
594
+ // Panel chrome — toast and centered share the same chip look,
595
+ // just position differs. We keep the slightly richer green tint
596
+ // for the HMR success state so it pops without needing the
597
+ // backdrop wash.
598
+ let panel = null;
594
599
  try {
595
- const panel = refs.titleLabel.parent;
600
+ panel = refs.titleLabel.parent;
596
601
  if (panel) {
597
- panel.backgroundColor = asColor('#FFFFFFFF');
602
+ const panelBg = snapshot.tone === 'success' ? '#E6F8E9FF' : '#FFFFFFFF';
603
+ panel.backgroundColor = asColor(panelBg);
598
604
  panel.opacity = 1;
599
605
  panel.padding = 16;
600
606
  try {
601
607
  panel.borderRadius = 12;
602
608
  }
603
609
  catch { }
610
+ // Position-aware alignment. The wrapper GridLayout fills the
611
+ // page content area. NativeScript runs every Android activity
612
+ // edge-to-edge, so that area extends under the status bar and
613
+ // navigation bar — we fold the system-bar safe-area insets into
614
+ // the chip's margin (via computeAndroidToastMargin) so a top
615
+ // chip clears the status bar and a bottom chip clears the nav
616
+ // bar instead of being clipped behind them. readAndroidSafeAreaInsets
617
+ // returns null off-Android, so the margins collapse to their base
618
+ // values (no behavioural change on iOS's in-tree fallback / tests).
619
+ try {
620
+ if (position === 'top' || position === 'bottom') {
621
+ const m = computeAndroidToastMargin({ position, safeInsets: readAndroidSafeAreaInsets() });
622
+ panel.verticalAlignment = position;
623
+ panel.margin = `${m.top} ${m.right} ${m.bottom} ${m.left}`;
624
+ }
625
+ else {
626
+ panel.verticalAlignment = 'middle';
627
+ panel.margin = 24;
628
+ }
629
+ }
630
+ catch { }
631
+ // Force a tight re-measure so the StackLayout panel hugs the
632
+ // CURRENT title+status text. Connection stages can swap between
633
+ // frames whose text wraps to a different number of lines (e.g. a
634
+ // long 'reconnecting' "Retrying ws://…" detail vs. the short
635
+ // 'offline' message). Without an explicit relayout the panel can
636
+ // retain the tallest height it ever measured, leaving an oversized
637
+ // gap between title and message. Mirrors updateBootStatusLabel.
638
+ try {
639
+ refs.titleLabel.requestLayout?.();
640
+ refs.statusLabel.requestLayout?.();
641
+ panel.requestLayout?.();
642
+ }
643
+ catch { }
604
644
  }
605
645
  }
606
646
  catch { }
647
+ // Touch passthrough for toast; centered mode keeps the
648
+ // blocking modal so the dim backdrop is meaningful.
649
+ try {
650
+ refs.overlay.isUserInteractionEnabled = !isToast;
651
+ }
652
+ catch { }
653
+ const positionChanged = previousPosition !== position;
654
+ const justAppeared = visible && (!wasVisible || positionChanged);
655
+ const justDismissed = !visible && wasVisible;
656
+ if (justAppeared) {
657
+ refs.overlay.visibility = 'visible';
658
+ if (isToast && panel && typeof panel.animate === 'function') {
659
+ animateLivePanelIn(panel, position);
660
+ }
661
+ else if (panel) {
662
+ try {
663
+ panel.translateY = 0;
664
+ panel.opacity = 1;
665
+ }
666
+ catch { }
667
+ }
668
+ }
669
+ else if (justDismissed) {
670
+ if (isToast && panel && typeof panel.animate === 'function') {
671
+ animateLivePanelOut(panel, previousPosition, () => {
672
+ try {
673
+ refs.overlay.visibility = 'collapse';
674
+ }
675
+ catch { }
676
+ });
677
+ }
678
+ else {
679
+ refs.overlay.visibility = 'collapse';
680
+ }
681
+ }
682
+ else {
683
+ // Steady-state refresh (e.g. offline → reconnecting → offline while
684
+ // already visible): no slide animation fires here, so clear any
685
+ // transform residue a previously-interrupted slide-in/out may have
686
+ // left on the panel. Otherwise the chip can appear shifted or
687
+ // faded across rapid connection-stage swaps.
688
+ if (visible && panel) {
689
+ try {
690
+ panel.translateY = 0;
691
+ panel.opacity = 1;
692
+ }
693
+ catch { }
694
+ }
695
+ refs.overlay.visibility = visible ? 'visible' : 'collapse';
696
+ }
697
+ if (typeof refs.wasVisible !== 'undefined')
698
+ refs.wasVisible = visible;
699
+ if (typeof refs.currentPosition !== 'undefined')
700
+ refs.currentPosition = position;
701
+ }
702
+ /**
703
+ * Slide-in animation for the in-tree toast panel.
704
+ *
705
+ * NativeScript's `View.animate({ translate, opacity, duration, curve })`
706
+ * is widely available across Core versions, so we don't depend on any
707
+ * specific curve enum being importable here. We use a moderate-to-snappy
708
+ * 320ms ease-out which feels close to a UIView spring without needing
709
+ * platform-specific APIs.
710
+ */
711
+ function animateLivePanelIn(panel, position) {
712
+ if (!panel || typeof panel.animate !== 'function')
713
+ return;
714
+ try {
715
+ const startY = position === 'bottom' ? 80 : -80;
716
+ panel.translateY = startY;
717
+ panel.opacity = 0;
718
+ const result = panel.animate({
719
+ translate: { x: 0, y: 0 },
720
+ opacity: 1,
721
+ duration: 320,
722
+ curve: 'easeOut',
723
+ });
724
+ if (result && typeof result.catch === 'function') {
725
+ result.catch(() => {
726
+ try {
727
+ panel.translateY = 0;
728
+ panel.opacity = 1;
729
+ }
730
+ catch { }
731
+ });
732
+ }
733
+ }
734
+ catch {
735
+ try {
736
+ panel.translateY = 0;
737
+ panel.opacity = 1;
738
+ }
739
+ catch { }
740
+ }
741
+ }
742
+ function animateLivePanelOut(panel, position, onComplete) {
743
+ if (!panel || typeof panel.animate !== 'function') {
744
+ onComplete();
745
+ return;
746
+ }
747
+ try {
748
+ const targetY = position === 'bottom' ? 80 : -80;
749
+ const result = panel.animate({
750
+ translate: { x: 0, y: targetY },
751
+ opacity: 0,
752
+ duration: 220,
753
+ curve: 'easeIn',
754
+ });
755
+ const finish = () => {
756
+ try {
757
+ panel.translateY = 0;
758
+ panel.opacity = 1;
759
+ }
760
+ catch { }
761
+ onComplete();
762
+ };
763
+ if (result && typeof result.then === 'function') {
764
+ result.then(finish, finish);
765
+ }
766
+ else {
767
+ finish();
768
+ }
769
+ }
770
+ catch {
771
+ onComplete();
772
+ }
773
+ }
774
+ // pure helpers for iOS window promotion. Factored out so the layout
775
+ // math and window-level selection stay unit-testable without booting a
776
+ // simulator. See `dev-overlay.spec.ts`.
777
+ /**
778
+ * Returns the UIWindow level we use for the live/connection overlay. We lift
779
+ * above `UIWindowLevelAlert` so system alerts (and any app-presented modal)
780
+ * stack underneath. When the platform does not expose `UIWindowLevelAlert`
781
+ * we fall back to the documented constant value (2000).
782
+ */
783
+ export function computeIosOverlayWindowLevel(baseAlert) {
784
+ if (typeof baseAlert === 'number' && Number.isFinite(baseAlert)) {
785
+ return baseAlert + 1;
786
+ }
787
+ return 2000 + 1;
788
+ }
789
+ /**
790
+ * Layout math for the live overlay when it runs inside its own UIWindow.
791
+ * Pure, deterministic and independent of UIKit so we can verify the rules
792
+ * (max panel width, position-aware placement, safe-area clamping, sane
793
+ * defaults) from tests.
794
+ *
795
+ * `position` controls where the panel sits vertically:
796
+ * - 'top': hugs `safeInsets.top + toastVerticalInset` so the chip
797
+ * sits just below the notch / Dynamic Island.
798
+ * - 'bottom': hugs `viewHeight - safeInsets.bottom - panelHeight -
799
+ * toastVerticalInset` so the chip sits just above the
800
+ * home indicator / nav bar.
801
+ * - 'center': original modal placement (vertically centered, clamped
802
+ * so it never crosses the top safe-area inset).
803
+ */
804
+ export function computeIosOverlayLayout(input) {
805
+ const viewWidth = Math.max(0, Number(input.viewWidth) || 0);
806
+ const viewHeight = Math.max(0, Number(input.viewHeight) || 0);
807
+ const safeInsets = {
808
+ top: Math.max(0, Number(input.safeInsets?.top ?? 0) || 0),
809
+ bottom: Math.max(0, Number(input.safeInsets?.bottom ?? 0) || 0),
810
+ left: Math.max(0, Number(input.safeInsets?.left ?? 0) || 0),
811
+ right: Math.max(0, Number(input.safeInsets?.right ?? 0) || 0),
812
+ };
813
+ const titleHeight = Math.max(0, Number(input.titleHeight) || 0);
814
+ const statusHeight = Math.max(0, Number(input.statusHeight) || 0);
815
+ const horizontalMargin = Math.max(0, Number(input.horizontalMargin ?? 24));
816
+ const maxPanelWidth = Math.max(0, Number(input.maxPanelWidth ?? 340));
817
+ const panelPadding = Math.max(0, Number(input.panelPadding ?? 16));
818
+ const interLabelSpacing = Math.max(0, Number(input.interLabelSpacing ?? 10));
819
+ const minTopInset = Math.max(0, Number(input.minTopInset ?? 20));
820
+ // Default to 'center' on the pure function so the existing
821
+ // snapshot/layout tests remain stable; the runtime call site
822
+ // (layoutIosOverlayRefs) reads the configured position from
823
+ // `getHmrDevOverlayPosition()` and forwards it explicitly.
824
+ const position = input.position ?? 'center';
825
+ // Distance between the panel and the safe-area edge in toast
826
+ // modes. 8pt mirrors the typical iOS notification chip inset and
827
+ // keeps the chip from hugging the notch / home indicator.
828
+ const toastVerticalInset = Math.max(0, Number(input.toastVerticalInset ?? 8));
829
+ const available = Math.max(0, viewWidth - 2 * horizontalMargin - safeInsets.left - safeInsets.right);
830
+ const panelWidth = Math.min(maxPanelWidth, available);
831
+ const innerWidth = Math.max(0, panelWidth - 2 * panelPadding);
832
+ const spacing = titleHeight > 0 && statusHeight > 0 ? interLabelSpacing : 0;
833
+ const panelHeight = panelPadding * 2 + titleHeight + spacing + statusHeight;
834
+ const panelX = Math.max(0, (viewWidth - panelWidth) / 2);
835
+ let panelY;
836
+ if (position === 'top') {
837
+ // Pin to the top safe-area inset (just below notch / Dynamic
838
+ // Island). Clamp non-negative for fully-NaN input.
839
+ panelY = Math.max(0, safeInsets.top + toastVerticalInset);
840
+ }
841
+ else if (position === 'bottom') {
842
+ // Pin to the bottom safe-area inset (just above home indicator
843
+ // / nav bar). If the panel can't fit between the safe-area
844
+ // insets we fall back to the top safe-area edge so the chip is
845
+ // always visible (rather than getting clipped off-screen).
846
+ const desired = viewHeight - safeInsets.bottom - panelHeight - toastVerticalInset;
847
+ panelY = Math.max(safeInsets.top + minTopInset, desired);
848
+ }
849
+ else {
850
+ // Center vertically, but never cross the top safe-area inset
851
+ // (notch/Dynamic Island). Original modal placement.
852
+ const centered = (viewHeight - panelHeight) / 2;
853
+ panelY = Math.max(safeInsets.top + minTopInset, centered);
854
+ }
855
+ return {
856
+ backdrop: { x: 0, y: 0, width: viewWidth, height: viewHeight },
857
+ panel: { x: panelX, y: panelY, width: panelWidth, height: panelHeight },
858
+ title: { x: panelPadding, y: panelPadding, width: innerWidth, height: titleHeight },
859
+ status: {
860
+ x: panelPadding,
861
+ y: panelPadding + titleHeight + spacing,
862
+ width: innerWidth,
863
+ height: statusHeight,
864
+ },
865
+ };
866
+ }
867
+ /**
868
+ * Margin math for the in-tree (Android) live/update toast chip.
869
+ *
870
+ * NativeScript enables edge-to-edge on every Android activity
871
+ * (`@nativescript/core` → `application.android.ts` → `enableEdgeToEdge`),
872
+ * so the page content — and therefore the overlay wrapper we inject into
873
+ * it — extends underneath the status bar and navigation bar. A fixed
874
+ * top/bottom margin leaves the chip tucked behind the system bars, where
875
+ * it gets clipped (Android 15 / API 35 enforces edge-to-edge, so this is
876
+ * now the norm rather than the exception).
877
+ *
878
+ * This pure helper folds the system-bar safe-area insets into the chip's
879
+ * margin so the toast always clears the status bar (top position) /
880
+ * navigation bar (bottom position). It is deterministic and free of
881
+ * native deps so the rules stay unit-testable — the runtime reads the
882
+ * live insets via `readAndroidSafeAreaInsets()` and forwards them here.
883
+ *
884
+ * - 'top': margin.top = baseVerticalInset + safeInsets.top
885
+ * - 'bottom': margin.bottom = baseVerticalInset + safeInsets.bottom
886
+ * - 'center': fixed `centerMargin` all around (vertically centered, so
887
+ * it never approaches the system bars on normal viewports).
888
+ *
889
+ * Horizontal margins always include the relevant safe-area inset so a
890
+ * landscape display cutout / gesture pill doesn't clip the chip edge.
891
+ */
892
+ export function computeAndroidToastMargin(input) {
893
+ // Clamp to a finite, non-negative number; fall back to `fallback`
894
+ // for missing/NaN input so a glitched measurement can't produce a
895
+ // negative or NaN margin (which NativeScript would silently drop).
896
+ const clamp = (value, fallback) => {
897
+ const n = Number(value);
898
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
899
+ };
900
+ const safe = {
901
+ top: clamp(input.safeInsets?.top, 0),
902
+ bottom: clamp(input.safeInsets?.bottom, 0),
903
+ left: clamp(input.safeInsets?.left, 0),
904
+ right: clamp(input.safeInsets?.right, 0),
905
+ };
906
+ const baseVerticalInset = clamp(input.baseVerticalInset, 8);
907
+ const baseHorizontalInset = clamp(input.baseHorizontalInset, 16);
908
+ const centerMargin = clamp(input.centerMargin, 24);
909
+ if (input.position === 'center') {
910
+ return { top: centerMargin, right: centerMargin, bottom: centerMargin, left: centerMargin };
911
+ }
912
+ const left = Math.round(baseHorizontalInset + safe.left);
913
+ const right = Math.round(baseHorizontalInset + safe.right);
914
+ if (input.position === 'bottom') {
915
+ return { top: 0, right, bottom: Math.round(baseVerticalInset + safe.bottom), left };
916
+ }
917
+ // 'top' (and any unexpected value) hugs the status-bar safe area.
918
+ return { top: Math.round(baseVerticalInset + safe.top), right, bottom: 0, left };
919
+ }
920
+ /**
921
+ * Returns the iOS UIKit symbols we rely on if we're running on an iOS runtime
922
+ * with the metadata bridge available. Returns null on Android, web, or in
923
+ * tests so callers can gracefully fall back to the in-tree overlay.
924
+ */
925
+ function getIosOverlayHost() {
926
+ const g = getOverlayGlobal();
927
+ if (typeof g.UIWindow === 'undefined' || typeof g.UIApplication === 'undefined' || typeof g.UIViewController === 'undefined' || typeof g.UIView === 'undefined' || typeof g.UILabel === 'undefined' || typeof g.UIColor === 'undefined' || typeof g.UIFont === 'undefined' || typeof g.UIScreen === 'undefined') {
928
+ return null;
929
+ }
930
+ return {
931
+ UIWindow: g.UIWindow,
932
+ UIViewController: g.UIViewController,
933
+ UIView: g.UIView,
934
+ UILabel: g.UILabel,
935
+ UIColor: g.UIColor,
936
+ UIFont: g.UIFont,
937
+ UIApplication: g.UIApplication,
938
+ UIScreen: g.UIScreen,
939
+ UIWindowLevelAlert: typeof g.UIWindowLevelAlert === 'number' ? g.UIWindowLevelAlert : undefined,
940
+ };
941
+ }
942
+ /**
943
+ * Walks UIApplication.sharedApplication windows and returns the first active
944
+ * UIWindowScene we can locate. On iOS 13+ every UIWindow is attached to a
945
+ * scene, and we must initialise our overlay window the same way or the OS
946
+ * will silently refuse to render it. Returns null when no scene is found
947
+ * (older iOS versions or non-UI environments).
948
+ */
949
+ function findActiveWindowScene(host) {
950
+ try {
951
+ const app = host.UIApplication.sharedApplication;
952
+ const windows = app?.windows;
953
+ if (!windows || typeof windows.count !== 'number')
954
+ return null;
955
+ for (let i = 0; i < windows.count; i++) {
956
+ const w = windows.objectAtIndex(i);
957
+ const scene = w && w.windowScene;
958
+ if (scene)
959
+ return scene;
960
+ }
961
+ }
962
+ catch { }
963
+ return null;
964
+ }
965
+ function buildIosOverlayRefs(state) {
966
+ const host = getIosOverlayHost();
967
+ if (!host)
968
+ return null;
969
+ // Without a scene we can't build a modern UIWindow that actually renders.
970
+ // Fall back to the in-tree overlay rather than show nothing.
971
+ const scene = findActiveWindowScene(host);
972
+ if (!scene) {
973
+ if (state.verbose) {
974
+ console.info('[ns-hmr-overlay] no active UIWindowScene; skipping iOS overlay promotion');
975
+ }
976
+ return null;
977
+ }
978
+ try {
979
+ const { UIWindow, UIViewController, UIView, UILabel, UIColor, UIFont } = host;
980
+ const window = UIWindow.alloc().initWithWindowScene(scene);
981
+ window.windowLevel = computeIosOverlayWindowLevel(host.UIWindowLevelAlert ?? null);
982
+ window.backgroundColor = UIColor.clearColor;
983
+ window.hidden = true;
984
+ const controller = UIViewController.new();
985
+ controller.view.backgroundColor = UIColor.clearColor;
986
+ window.rootViewController = controller;
987
+ // UIViewAutoresizing bit masks. We mirror the UIKit constants here to
988
+ // avoid depending on symbols the metadata bridge does not always
989
+ // expose as top-level globals.
990
+ const FLEXIBLE_LEFT_MARGIN = 1 << 0;
991
+ const FLEXIBLE_WIDTH = 1 << 1;
992
+ const FLEXIBLE_RIGHT_MARGIN = 1 << 2;
993
+ const FLEXIBLE_TOP_MARGIN = 1 << 3;
994
+ const FLEXIBLE_HEIGHT = 1 << 4;
995
+ const FLEXIBLE_BOTTOM_MARGIN = 1 << 5;
996
+ const backdrop = UIView.new();
997
+ backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0, 0, 0.35);
998
+ backdrop.autoresizingMask = FLEXIBLE_WIDTH | FLEXIBLE_HEIGHT;
999
+ controller.view.addSubview(backdrop);
1000
+ const panel = UIView.new();
1001
+ panel.backgroundColor = UIColor.whiteColor;
1002
+ panel.autoresizingMask = FLEXIBLE_LEFT_MARGIN | FLEXIBLE_RIGHT_MARGIN | FLEXIBLE_TOP_MARGIN | FLEXIBLE_BOTTOM_MARGIN;
1003
+ try {
1004
+ panel.layer.cornerRadius = 14;
1005
+ panel.layer.masksToBounds = true;
1006
+ }
1007
+ catch { }
1008
+ controller.view.addSubview(panel);
1009
+ const titleLabel = UILabel.new();
1010
+ titleLabel.numberOfLines = 0;
1011
+ titleLabel.textAlignment = 1; // NSTextAlignmentCenter
1012
+ titleLabel.font = UIFont.boldSystemFontOfSize(16);
1013
+ titleLabel.textColor = UIColor.blackColor;
1014
+ panel.addSubview(titleLabel);
1015
+ const statusLabel = UILabel.new();
1016
+ statusLabel.numberOfLines = 0;
1017
+ statusLabel.textAlignment = 1;
1018
+ statusLabel.font = UIFont.systemFontOfSize(13);
1019
+ statusLabel.textColor = UIColor.darkGrayColor;
1020
+ panel.addSubview(statusLabel);
1021
+ // Subtle drop-shadow so the toast chip reads against light app
1022
+ // content (white-on-white is invisible). The error / centered
1023
+ // branches still get the dim backdrop, so the shadow is mostly
1024
+ // a no-op for them — but it's a one-time setup.
1025
+ try {
1026
+ panel.layer.shadowColor = UIColor.blackColor.CGColor;
1027
+ panel.layer.shadowOpacity = 0.18;
1028
+ panel.layer.shadowRadius = 8;
1029
+ panel.layer.shadowOffset = { width: 0, height: 2 };
1030
+ panel.layer.masksToBounds = false;
1031
+ }
1032
+ catch { }
1033
+ // `wasVisible` / `currentPosition` are mutated by
1034
+ // applySnapshotToIosRefs when the snapshot triggers a slide-in
1035
+ // or slide-out. They start in the "hidden" state so the very
1036
+ // first visible snapshot animates in cleanly.
1037
+ return {
1038
+ window,
1039
+ controller,
1040
+ backdrop,
1041
+ panel,
1042
+ titleLabel,
1043
+ statusLabel,
1044
+ wasVisible: false,
1045
+ currentPosition: getHmrDevOverlayPosition(),
1046
+ };
1047
+ }
1048
+ catch (err) {
1049
+ console.warn('[ns-hmr-overlay] iOS overlay construction failed:', err?.message || err);
1050
+ return null;
1051
+ }
1052
+ }
1053
+ function ensureIosOverlayRefs(state) {
1054
+ if (state.iosRefs)
1055
+ return state.iosRefs;
1056
+ if (state.iosBuildFailed)
1057
+ return null;
1058
+ const built = buildIosOverlayRefs(state);
1059
+ if (built) {
1060
+ state.iosRefs = built;
1061
+ }
1062
+ else {
1063
+ // Remember failure so we don't hammer construction on every snapshot
1064
+ // update — the in-tree path will take over for this session.
1065
+ state.iosBuildFailed = true;
1066
+ }
1067
+ return state.iosRefs;
1068
+ }
1069
+ function layoutIosOverlayRefs(refs, position) {
1070
+ try {
1071
+ const bounds = refs.controller.view.bounds;
1072
+ const viewWidth = Number(bounds?.size?.width) || 0;
1073
+ const viewHeight = Number(bounds?.size?.height) || 0;
1074
+ const raw = refs.controller.view.safeAreaInsets;
1075
+ const safeInsets = raw
1076
+ ? {
1077
+ top: Number(raw.top) || 0,
1078
+ bottom: Number(raw.bottom) || 0,
1079
+ left: Number(raw.left) || 0,
1080
+ right: Number(raw.right) || 0,
1081
+ }
1082
+ : { top: 0, bottom: 0, left: 0, right: 0 };
1083
+ // Ask UIKit what the labels want given the panel inner width. We use a
1084
+ // generous height bound so nothing clips on long reconnect strings.
1085
+ const panelPadding = 16;
1086
+ const horizontalMargin = 24;
1087
+ const maxPanelWidth = 340;
1088
+ const innerWidth = Math.max(0, Math.min(maxPanelWidth, viewWidth - 2 * horizontalMargin - safeInsets.left - safeInsets.right) - 2 * panelPadding);
1089
+ const titleFit = refs.titleLabel.sizeThatFits({ width: innerWidth, height: 10000 }) || { height: 0 };
1090
+ const statusFit = refs.statusLabel.sizeThatFits({ width: innerWidth, height: 10000 }) || { height: 0 };
1091
+ const layout = computeIosOverlayLayout({
1092
+ viewWidth,
1093
+ viewHeight,
1094
+ safeInsets,
1095
+ titleHeight: Number(titleFit.height) || 0,
1096
+ statusHeight: Number(statusFit.height) || 0,
1097
+ maxPanelWidth,
1098
+ horizontalMargin,
1099
+ panelPadding,
1100
+ position,
1101
+ });
1102
+ const toCgRect = (rect) => ({
1103
+ origin: { x: rect.x, y: rect.y },
1104
+ size: { width: rect.width, height: rect.height },
1105
+ });
1106
+ refs.backdrop.frame = toCgRect(layout.backdrop);
1107
+ refs.panel.frame = toCgRect(layout.panel);
1108
+ refs.titleLabel.frame = toCgRect(layout.title);
1109
+ refs.statusLabel.frame = toCgRect(layout.status);
1110
+ return layout;
1111
+ }
1112
+ catch (err) {
1113
+ console.warn('[ns-hmr-overlay] iOS overlay layout failed:', err?.message || err);
1114
+ return null;
1115
+ }
1116
+ }
1117
+ /**
1118
+ * Slide-in animation for the iOS toast panel. Off-screen start frame
1119
+ * lives just above (top) or below (bottom) the visible area; the panel
1120
+ * snaps to its target frame with a spring so the motion feels physical
1121
+ * without the heavy "settle" overshoot of a hard spring (damping 0.85
1122
+ * lands quickly with a small overshoot).
1123
+ */
1124
+ function animateIosPanelIn(refs, position, layout) {
1125
+ const g = getOverlayGlobal();
1126
+ const UIView = g?.UIView;
1127
+ if (!UIView)
1128
+ return;
1129
+ try {
1130
+ const targetFrame = {
1131
+ origin: { x: layout.panel.x, y: layout.panel.y },
1132
+ size: { width: layout.panel.width, height: layout.panel.height },
1133
+ };
1134
+ // Off-screen start: distance includes a small fudge so the
1135
+ // shadow blur tail isn't visible at t=0.
1136
+ const startY = position === 'bottom' ? layout.backdrop.height + 24 : -(layout.panel.height + 24);
1137
+ refs.panel.frame = {
1138
+ origin: { x: layout.panel.x, y: startY },
1139
+ size: { width: layout.panel.width, height: layout.panel.height },
1140
+ };
1141
+ refs.panel.alpha = 0;
1142
+ try {
1143
+ if (typeof UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion === 'function') {
1144
+ UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(0.42, 0, 0.85, 0.7, 0, () => {
1145
+ refs.panel.frame = targetFrame;
1146
+ refs.panel.alpha = 1;
1147
+ }, null);
1148
+ }
1149
+ else if (typeof UIView.animateWithDurationAnimations === 'function') {
1150
+ UIView.animateWithDurationAnimations(0.32, () => {
1151
+ refs.panel.frame = targetFrame;
1152
+ refs.panel.alpha = 1;
1153
+ });
1154
+ }
1155
+ else {
1156
+ refs.panel.frame = targetFrame;
1157
+ refs.panel.alpha = 1;
1158
+ }
1159
+ }
1160
+ catch {
1161
+ refs.panel.frame = targetFrame;
1162
+ refs.panel.alpha = 1;
1163
+ }
1164
+ }
1165
+ catch { }
1166
+ }
1167
+ /**
1168
+ * Slide-out animation for the iOS toast panel. Mirrors animateIosPanelIn:
1169
+ * the panel travels to the nearest off-screen edge while fading out so
1170
+ * the dismissal still feels intentional even on fast HMR cycles.
1171
+ */
1172
+ function animateIosPanelOut(refs, position, onComplete) {
1173
+ const g = getOverlayGlobal();
1174
+ const UIView = g?.UIView;
1175
+ const currentFrame = refs.panel?.frame;
1176
+ if (!UIView || !currentFrame) {
1177
+ onComplete();
1178
+ return;
1179
+ }
1180
+ try {
1181
+ const bounds = refs.controller?.view?.bounds;
1182
+ const viewHeight = Number(bounds?.size?.height) || 0;
1183
+ const targetY = position === 'bottom' ? viewHeight + 24 : -(Number(currentFrame.size?.height) + 24);
1184
+ const startFrame = currentFrame;
1185
+ const targetFrame = {
1186
+ origin: { x: Number(startFrame.origin?.x) || 0, y: targetY },
1187
+ size: startFrame.size,
1188
+ };
1189
+ try {
1190
+ if (typeof UIView.animateWithDurationDelayOptionsAnimationsCompletion === 'function') {
1191
+ // UIViewAnimationOptionCurveEaseIn = 1 << 16 — accelerate
1192
+ // out so the dismissal doesn't drag on screen.
1193
+ UIView.animateWithDurationDelayOptionsAnimationsCompletion(0.22, 0, 1 << 16, () => {
1194
+ refs.panel.frame = targetFrame;
1195
+ refs.panel.alpha = 0;
1196
+ }, () => onComplete());
1197
+ }
1198
+ else if (typeof UIView.animateWithDurationAnimationsCompletion === 'function') {
1199
+ UIView.animateWithDurationAnimationsCompletion(0.22, () => {
1200
+ refs.panel.frame = targetFrame;
1201
+ refs.panel.alpha = 0;
1202
+ }, () => onComplete());
1203
+ }
1204
+ else {
1205
+ refs.panel.alpha = 0;
1206
+ onComplete();
1207
+ }
1208
+ }
1209
+ catch {
1210
+ refs.panel.alpha = 0;
1211
+ onComplete();
1212
+ }
1213
+ }
1214
+ catch {
1215
+ onComplete();
1216
+ }
1217
+ }
1218
+ function applySnapshotToIosRefs(refs, snapshot) {
1219
+ if (!refs)
1220
+ return false;
1221
+ try {
1222
+ // 'update' mode rides the same dedicated UIWindow as
1223
+ // 'connection' so the HMR apply overlay always stacks above
1224
+ // modals/sheets/system alerts. The window is constructed
1225
+ // lazily (ensureIosOverlayRefs) and reused for the lifetime of
1226
+ // the dev session.
1227
+ const visible = snapshot.visible && (snapshot.mode === 'connection' || snapshot.mode === 'update');
1228
+ const wasVisible = !!refs.wasVisible;
1229
+ const position = getHmrDevOverlayPosition();
1230
+ const previousPosition = refs.currentPosition;
1231
+ const isToast = position !== 'center';
1232
+ // Touches pass through the overlay window in toast mode so
1233
+ // the user can keep tapping the app while the HMR chip is
1234
+ // shown. In centered mode we keep the blocking
1235
+ // behaviour (the dim backdrop is itself a hint to wait).
1236
+ try {
1237
+ refs.window.userInteractionEnabled = !isToast;
1238
+ }
1239
+ catch { }
1240
+ if (!visible) {
1241
+ // Animate out before hiding the window so the dismissal
1242
+ // has a discoverable motion. Only animate when previously
1243
+ // visible and in toast mode — centered modal hides instantly.
1244
+ if (wasVisible && isToast) {
1245
+ animateIosPanelOut(refs, previousPosition, () => {
1246
+ try {
1247
+ refs.window.hidden = true;
1248
+ }
1249
+ catch { }
1250
+ });
1251
+ }
1252
+ else {
1253
+ refs.window.hidden = true;
1254
+ }
1255
+ refs.wasVisible = false;
1256
+ refs.currentPosition = position;
1257
+ return true;
1258
+ }
1259
+ refs.window.hidden = false;
1260
+ refs.titleLabel.text = snapshot.title || '';
1261
+ refs.statusLabel.text = formatStatusText(snapshot);
1262
+ const host = getIosOverlayHost();
1263
+ if (host) {
1264
+ const { UIColor } = host;
1265
+ const isError = snapshot.tone === 'error';
1266
+ const isSuccess = snapshot.tone === 'success';
1267
+ try {
1268
+ if (isError) {
1269
+ // Red panel + dark red text (existing UX).
1270
+ refs.panel.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(1, 0.96, 0.96, 1);
1271
+ refs.titleLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.7, 0.1, 0.06, 1);
1272
+ refs.statusLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.7, 0.1, 0.06, 0.9);
1273
+ }
1274
+ else if (isSuccess) {
1275
+ // Slightly more saturated green panel + dark-green
1276
+ // text. The previous 0.94/0.99/0.95 background was
1277
+ // nearly indistinguishable from white on most
1278
+ // devices; this bump keeps long detail strings
1279
+ // readable while making the apply event obviously
1280
+ // "happening right now".
1281
+ refs.panel.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0.9, 0.97, 0.91, 1);
1282
+ refs.titleLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.05, 0.43, 0.18, 1);
1283
+ refs.statusLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.05, 0.43, 0.18, 1);
1284
+ }
1285
+ else {
1286
+ // Default (info / warn) — existing connection look.
1287
+ refs.panel.backgroundColor = UIColor.whiteColor;
1288
+ refs.titleLabel.textColor = UIColor.blackColor;
1289
+ refs.statusLabel.textColor = UIColor.darkGrayColor;
1290
+ }
1291
+ // Backdrop dims only in centered mode; toast mode keeps
1292
+ // the rest of the app fully visible/usable. Errors get
1293
+ // a slightly stronger dim in centered mode because the
1294
+ // user MUST notice them.
1295
+ if (isToast) {
1296
+ refs.backdrop.backgroundColor = UIColor.clearColor;
1297
+ }
1298
+ else if (isError) {
1299
+ refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0, 0, 0.35);
1300
+ }
1301
+ else if (isSuccess) {
1302
+ refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0.15, 0.05, 0.28);
1303
+ }
1304
+ else {
1305
+ refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0, 0, 0.35);
1306
+ }
1307
+ }
1308
+ catch { }
1309
+ }
1310
+ const layout = layoutIosOverlayRefs(refs, position);
1311
+ // Slide-in animation only fires on the actual hidden→visible
1312
+ // transition (or on a position swap — e.g. dev toggling top
1313
+ // to bottom mid-cycle). Subsequent updates within the same
1314
+ // visible cycle just refresh text/colours without re-animating.
1315
+ const positionChanged = previousPosition !== position;
1316
+ const justAppeared = !wasVisible || positionChanged;
1317
+ if (justAppeared && isToast && layout) {
1318
+ animateIosPanelIn(refs, position, layout);
1319
+ }
1320
+ else if (justAppeared && !isToast) {
1321
+ // Centered modal: ensure alpha is reset to 1 in case a
1322
+ // previous toast-mode dismissal left it at 0.
1323
+ try {
1324
+ refs.panel.alpha = 1;
1325
+ }
1326
+ catch { }
1327
+ }
1328
+ refs.wasVisible = true;
1329
+ refs.currentPosition = position;
1330
+ return true;
1331
+ }
1332
+ catch (err) {
1333
+ console.warn('[ns-hmr-overlay] iOS overlay apply failed:', err?.message || err);
1334
+ return false;
1335
+ }
607
1336
  }
608
1337
  function applyRuntimeSnapshot(snapshot) {
609
1338
  const state = getRuntimeState();
@@ -613,15 +1342,108 @@ function applyRuntimeSnapshot(snapshot) {
613
1342
  updateBootStatusLabel(snapshot);
614
1343
  }
615
1344
  applySnapshotToBootRefs(state.bootRefs, snapshot);
616
- if (snapshot.visible && snapshot.mode === 'connection') {
617
- const liveRefs = ensureLiveOverlayRefs(snapshot);
618
- applySnapshotToLiveRefs(liveRefs, snapshot);
1345
+ // prefer the dedicated UIWindow
1346
+ // path so the live/update overlays always stack on top of modals,
1347
+ // sheets, and other windows. Fall back to the in-tree overlay when
1348
+ // iOS APIs aren't available (Android, tests, or when scene
1349
+ // construction fails).
1350
+ let handledByIos = false;
1351
+ // Both 'connection' and 'update' use the small-panel surface
1352
+ // (UIWindow on iOS, in-tree overlay everywhere else). 'boot' uses
1353
+ // the placeholder root via applySnapshotToBootRefs above; 'hidden'
1354
+ // hides everything.
1355
+ const wantsOverlay = snapshot.visible && (snapshot.mode === 'connection' || snapshot.mode === 'update');
1356
+ if (getIosOverlayHost()) {
1357
+ if (wantsOverlay) {
1358
+ const iosRefs = ensureIosOverlayRefs(state);
1359
+ handledByIos = applySnapshotToIosRefs(iosRefs, snapshot);
1360
+ }
1361
+ else if (state.iosRefs) {
1362
+ handledByIos = applySnapshotToIosRefs(state.iosRefs, snapshot);
1363
+ }
619
1364
  }
620
- else {
621
- applySnapshotToLiveRefs(state.liveRefs, snapshot);
1365
+ if (!handledByIos) {
1366
+ if (wantsOverlay) {
1367
+ const liveRefs = ensureLiveOverlayRefs(snapshot);
1368
+ applySnapshotToLiveRefs(liveRefs, snapshot);
1369
+ }
1370
+ else {
1371
+ applySnapshotToLiveRefs(state.liveRefs, snapshot);
1372
+ }
622
1373
  }
623
1374
  return state.snapshot;
624
1375
  }
1376
+ // How long the 'complete' frame stays on screen before we auto-hide.
1377
+ // The original 350ms was too tight: many HMR cycles complete in
1378
+ // 50–250ms, so the *total* overlay lifetime (received → complete +
1379
+ // 350ms) was often under 500ms, which is faster than the human eye
1380
+ // can comfortably register. 600ms gives the user time to read the
1381
+ // "Total Xms" line and confirm visually that something happened.
1382
+ const UPDATE_AUTO_HIDE_MS = 600;
1383
+ // Minimum perceptible duration for an entire update overlay cycle
1384
+ // (from 'received' to hide). If the cycle finished in 50ms (e.g., a
1385
+ // tiny HTML edit on a warm cache), we still hold for ~MIN_VISIBLE_MS
1386
+ // total before hiding so the overlay is actually seen. Combined with
1387
+ // UPDATE_AUTO_HIDE_MS, the *effective* hold-after-complete =
1388
+ // max(UPDATE_AUTO_HIDE_MS, MIN_VISIBLE_MS - elapsed-since-received).
1389
+ const UPDATE_MIN_VISIBLE_MS = 800;
1390
+ function clearUpdateAutoHideTimer(state) {
1391
+ if (state.updateAutoHideTimer) {
1392
+ try {
1393
+ clearTimeout(state.updateAutoHideTimer);
1394
+ }
1395
+ catch { }
1396
+ state.updateAutoHideTimer = null;
1397
+ }
1398
+ }
1399
+ function scheduleUpdateAutoHide(state) {
1400
+ clearUpdateAutoHideTimer(state);
1401
+ // Compute how much longer we need to hold the overlay so that the
1402
+ // total cycle visibility is at least UPDATE_MIN_VISIBLE_MS. For
1403
+ // fast cycles (50ms reboot) this stretches the hide; for slow
1404
+ // cycles (>UPDATE_MIN_VISIBLE_MS) it falls back to the standard
1405
+ // UPDATE_AUTO_HIDE_MS so we don't truncate the celebratory hold.
1406
+ const startedAt = state.updateCycleStartedAt || 0;
1407
+ const elapsed = startedAt > 0 ? Math.max(0, Date.now() - startedAt) : 0;
1408
+ const minRemainder = elapsed > 0 ? Math.max(0, UPDATE_MIN_VISIBLE_MS - elapsed) : UPDATE_MIN_VISIBLE_MS;
1409
+ const holdMs = Math.max(UPDATE_AUTO_HIDE_MS, minRemainder);
1410
+ try {
1411
+ state.updateAutoHideTimer = setTimeout(() => {
1412
+ state.updateAutoHideTimer = null;
1413
+ // Critical: only auto-hide if we're still on the 'complete'
1414
+ // frame. If a new HMR cycle has rotated the snapshot back
1415
+ // to 'update' / 'received' (e.g., user saved twice in
1416
+ // quick succession), the new cycle owns the overlay and
1417
+ // our timer must not steal it.
1418
+ const current = state.snapshot;
1419
+ if (current.mode === 'update' && current.tone === 'success' && current.progress === 100) {
1420
+ state.updateCycleStartedAt = 0;
1421
+ applyRuntimeSnapshot({ ...DEFAULT_SNAPSHOT });
1422
+ }
1423
+ }, holdMs);
1424
+ }
1425
+ catch {
1426
+ // setTimeout missing (extremely rare; some test envs). Fall
1427
+ // back to immediate hide so we never leave the overlay visible
1428
+ // forever after a 'complete'.
1429
+ state.updateCycleStartedAt = 0;
1430
+ applyRuntimeSnapshot({ ...DEFAULT_SNAPSHOT });
1431
+ }
1432
+ }
1433
+ function logUpdateStageTransition(state, stage, info) {
1434
+ if (!state.verbose)
1435
+ return;
1436
+ try {
1437
+ const detail = info?.detail || '';
1438
+ const progress = typeof info?.progress === 'number' ? info.progress : null;
1439
+ const progressTag = progress !== null ? ` (${Math.round(progress)}%)` : '';
1440
+ // Single-line breadcrumb so a developer can correlate
1441
+ // overlay frames with the [ns-hmr][angular] timing log when
1442
+ // debugging "I don't see the overlay" reports.
1443
+ console.info(`[ns-hmr-overlay] update stage=${stage}${progressTag}${detail ? ` detail=${detail}` : ''}`);
1444
+ }
1445
+ catch { }
1446
+ }
625
1447
  function createOverlayApi() {
626
1448
  return {
627
1449
  ensureBootPage(verbose) {
@@ -637,12 +1459,74 @@ function createOverlayApi() {
637
1459
  return state.bootRefs?.page || null;
638
1460
  },
639
1461
  setBootStage(stage, info) {
640
- return applyRuntimeSnapshot(createBootOverlaySnapshot(stage, info));
1462
+ // A boot transition cancels any pending HMR auto-hide so
1463
+ // the boot phase always wins.
1464
+ const state = getRuntimeState();
1465
+ clearUpdateAutoHideTimer(state);
1466
+ state.updateCycleStartedAt = 0;
1467
+ const next = createBootOverlaySnapshot(stage, info);
1468
+ // Monotonic boot-progress ratchet: boot stages can fire out of
1469
+ // order across boot paths (native `__nsStartDevSession` vs the
1470
+ // http-bootloader fallback) and individual bases were tuned
1471
+ // independently, so clamp boot→boot transitions to never go
1472
+ // backwards. Non-boot snapshots (error/ready) bypass — they
1473
+ // genuinely want to reset the visual.
1474
+ if (next.mode === 'boot' && state.snapshot.mode === 'boot' && typeof next.progress === 'number' && typeof state.snapshot.progress === 'number' && next.progress < state.snapshot.progress) {
1475
+ next.progress = state.snapshot.progress;
1476
+ }
1477
+ return applyRuntimeSnapshot(next);
641
1478
  },
642
1479
  setConnectionStage(stage, info) {
1480
+ const state = getRuntimeState();
1481
+ clearUpdateAutoHideTimer(state);
1482
+ state.updateCycleStartedAt = 0;
643
1483
  return applyRuntimeSnapshot(createConnectionOverlaySnapshot(stage, info));
644
1484
  },
1485
+ setUpdateStage(stage, info) {
1486
+ const state = getRuntimeState();
1487
+ // Each new in-progress stage cancels any pending auto-hide
1488
+ // from a previous cycle. Without this, two saves in quick
1489
+ // succession could see cycle-2's progress overlay yanked
1490
+ // off by cycle-1's already-scheduled hide.
1491
+ clearUpdateAutoHideTimer(state);
1492
+ // Stamp the cycle start on 'received', but distinguish
1493
+ // between two cases:
1494
+ //
1495
+ // (a) Re-assertion of the SAME cycle (e.g., the server
1496
+ // emits both `ns:hmr-pending` AND `ns:angular-update`,
1497
+ // both of which call `setUpdateStage('received')`).
1498
+ // We must PRESERVE the original timestamp so the
1499
+ // minimum-visible-window math measures the FIRST
1500
+ // 'received' the user actually saw.
1501
+ //
1502
+ // (b) Genuinely-new cycle starting either from a hidden
1503
+ // overlay OR while the previous cycle is still on
1504
+ // its 'complete' frame (pre auto-hide). In both
1505
+ // sub-cases we MUST stamp a fresh start so the
1506
+ // new cycle's auto-hide math is sane.
1507
+ //
1508
+ // We treat the previous snapshot as "in-progress for the
1509
+ // same cycle" iff mode==='update' AND progress!==100.
1510
+ // 'complete' frames are a sign that the cycle finished;
1511
+ // any subsequent 'received' is a NEW cycle.
1512
+ if (stage === 'received') {
1513
+ const prev = state.snapshot;
1514
+ const isMidCycleReassertion = prev.mode === 'update' && prev.progress !== 100;
1515
+ if (!isMidCycleReassertion) {
1516
+ state.updateCycleStartedAt = Date.now();
1517
+ }
1518
+ }
1519
+ logUpdateStageTransition(state, stage, info);
1520
+ const snapshot = applyRuntimeSnapshot(createUpdateOverlaySnapshot(stage, info));
1521
+ if (stage === 'complete') {
1522
+ scheduleUpdateAutoHide(state);
1523
+ }
1524
+ return snapshot;
1525
+ },
645
1526
  hide() {
1527
+ const state = getRuntimeState();
1528
+ clearUpdateAutoHideTimer(state);
1529
+ state.updateCycleStartedAt = 0;
646
1530
  applyRuntimeSnapshot({ ...DEFAULT_SNAPSHOT });
647
1531
  },
648
1532
  getSnapshot() {
@@ -668,6 +1552,14 @@ export function setHmrBootStage(stage, info) {
668
1552
  export function setHmrConnectionStage(stage, info) {
669
1553
  return ensureHmrDevOverlayRuntimeInstalled().setConnectionStage(stage, info);
670
1554
  }
1555
+ // Public entry point for driving the HMR-applying overlay. Callers
1556
+ // walk through stages (received → evicting → reimporting → rebooting
1557
+ // → complete); 'complete' auto-hides after a short interval.
1558
+ // Soft-fails (no-op) if the runtime overlay was never installed
1559
+ // (e.g., production builds, test environments).
1560
+ export function setHmrUpdateStage(stage, info) {
1561
+ return ensureHmrDevOverlayRuntimeInstalled().setUpdateStage(stage, info);
1562
+ }
671
1563
  export function hideHmrDevOverlay(reason) {
672
1564
  void reason;
673
1565
  ensureHmrDevOverlayRuntimeInstalled().hide(reason);