@@ -86,6 +86,29 @@ export const exportMapToHTML = (options, version = KEPLER_GL_VERSION) => {
8686 // native loader (which fails under file://).
8787 window.esmsInitOptions = {shimMode: true};
8888 </script>
89+ <script>
90+ // Replace the loading overlay with an error message.
91+ // Safe to call at any time — once React has mounted and removed
92+ // #kepler-load-screen this becomes a no-op.
93+ window._keplerShowLoadError = function(msg) {
94+ var loadScreen = document.getElementById('kepler-load-screen');
95+ if (!loadScreen) return;
96+ var spinner = loadScreen.querySelector('.kepler-spinner');
97+ var label = loadScreen.querySelector('.kepler-load-label');
98+ if (spinner) spinner.style.display = 'none';
99+ if (label) label.style.display = 'none';
100+ var errEl = document.createElement('div');
101+ errEl.className = 'kepler-load-error';
102+ errEl.textContent = msg || 'Failed to load the map. Check your internet connection and reload the page.';
103+ loadScreen.appendChild(errEl);
104+ };
105+ // Catch unhandled promise rejections (e.g. ES module imports failing
106+ // inside the module-shim script before loadScript is ever called).
107+ window.addEventListener('unhandledrejection', function(event) {
108+ console.error('kepler.gl: unhandled rejection during map load', event.reason);
109+ window._keplerShowLoadError('Failed to load the map. Check your internet connection and reload the page.');
110+ });
111+ </script>
89112 <script src="https://unpkg.com/es-module-shims@${ ES_MODULE_SHIMS_VERSION } /dist/es-module-shims.js" crossorigin="anonymous"></script>
90113
91114 <script type="importmap-shim">
@@ -155,10 +178,44 @@ export const exportMapToHTML = (options, version = KEPLER_GL_VERSION) => {
155178 })
156179 .catch(function(error) {
157180 console.error('kepler.gl: failed to load UMD bundle', error);
181+ window._keplerShowLoadError('Failed to load the map bundle. Check your internet connection and reload the page.');
158182 });
159183 </script>
160184 <style type="text/css">
161185 body {margin: 0; padding: 0; overflow: hidden;}
186+ #kepler-load-screen {
187+ position: fixed;
188+ top: 0; left: 0;
189+ width: 100%; height: 100%;
190+ background: #29323c;
191+ display: flex;
192+ flex-direction: column;
193+ align-items: center;
194+ justify-content: center;
195+ gap: 20px;
196+ z-index: 9999;
197+ font-family: ff-clan-web-pro, 'Helvetica Neue', Helvetica, sans-serif;
198+ color: #a0a7b4;
199+ }
200+ #kepler-load-screen .kepler-spinner {
201+ width: 48px; height: 48px;
202+ border: 4px solid rgba(255,255,255,0.08);
203+ border-top-color: #1fbad6;
204+ border-radius: 50%;
205+ animation: kepler-spin 0.8s linear infinite;
206+ }
207+ #kepler-load-screen .kepler-load-label {
208+ font-size: 13px;
209+ letter-spacing: 0.5px;
210+ }
211+ #kepler-load-screen .kepler-load-error {
212+ font-size: 13px;
213+ color: #f05e45;
214+ max-width: 360px;
215+ text-align: center;
216+ line-height: 1.5;
217+ }
218+ @keyframes kepler-spin { to { transform: rotate(360deg); } }
162219 </style>
163220
164221 <!--MapBox token-->
@@ -199,7 +256,10 @@ export const exportMapToHTML = (options, version = KEPLER_GL_VERSION) => {
199256 <body>
200257 <!-- We will put our React component inside this div. -->
201258 <div id="app">
202- <!-- Kepler.gl map will be placed here-->
259+ <div id="kepler-load-screen">
260+ <div class="kepler-spinner"></div>
261+ <div class="kepler-load-label">Loading map…</div>
262+ </div>
203263 </div>
204264
205265 <!--
@@ -214,17 +274,23 @@ export const exportMapToHTML = (options, version = KEPLER_GL_VERSION) => {
214274 }
215275
216276 /** STORE **/
217- const reducers = (function createReducers(redux, keplerGl) {
277+ // Pre-seed styleType so the correct basemap fetch starts on the very
278+ // first render, avoiding a flash of the default dark basemap.
279+ // options.config has the versioned envelope shape: { version, config: { mapStyle, ... } }
280+ const savedStyleType = ${ JSON . stringify ( options . config ?. config ?. mapStyle ?. styleType ?? null ) } ;
281+
282+ const reducers = (function createReducers(redux, keplerGl, styleType) {
218283 return redux.combineReducers({
219284 // mount keplerGl reducer
220285 keplerGl: keplerGl.keplerGlReducer.initialState({
221286 uiState: {
222287 readOnly: ${ options . mode === EXPORT_HTML_MAP_MODES . READ } ,
223288 currentModal: null
224- }
289+ },
290+ ...(styleType ? {mapStyle: {styleType}} : {})
225291 })
226292 });
227- }(Redux, KeplerGl));
293+ }(Redux, KeplerGl, savedStyleType ));
228294
229295 const middleWares = (function createMiddlewares(keplerGl) {
230296 return keplerGl.enhanceReduxMiddleware([
@@ -237,11 +303,9 @@ export const exportMapToHTML = (options, version = KEPLER_GL_VERSION) => {
237303 }(Redux, middleWares));
238304
239305 const store = (function createStore(redux, enhancers) {
240- const initialState = {};
241-
242306 return redux.createStore(
243307 reducers,
244- initialState ,
308+ {} ,
245309 redux.compose(enhancers)
246310 );
247311 }(Redux, enhancers));
@@ -359,6 +423,13 @@ export const exportMapToHTML = (options, version = KEPLER_GL_VERSION) => {
359423 window.addEventListener('resize', handleResize);
360424 return function() {window.removeEventListener('resize', handleResize);};
361425 }, []);
426+ // Remove the loading overlay after the first render is painted.
427+ // useEffect fires post-commit, so this is safe under React 19's
428+ // concurrent renderer — the map is visible before the overlay goes away.
429+ react.useEffect(function removeLoadScreen() {
430+ var loadScreen = document.getElementById('kepler-load-screen');
431+ if (loadScreen) loadScreen.remove();
432+ }, []);
362433 return react.createElement(
363434 'div',
364435 {style: {position: 'absolute', left: 0, width: '100vw', height: '100vh'}},
@@ -403,7 +474,11 @@ export const exportMapToHTML = (options, version = KEPLER_GL_VERSION) => {
403474 config
404475 );
405476
406- // For some reason Kepler overwrites the config without extra wait time
477+ // The 500 ms delay is a historical workaround for a race where
478+ // keplerGl re-initialises and overwrites config applied synchronously.
479+ // The pre-seeded styleType above removes the basemap flash on first
480+ // render; this dispatch still applies the full saved config (layers,
481+ // viewport, layer groups, etc.) once the component has settled.
407482 window.setTimeout(() => {
408483 store.dispatch(
409484 keplerGl.addDataToMap({
0 commit comments